@@ -24,8 +30,30 @@ function PostsLayoutComponent() {
}}
className="block py-1 text-blue-600 hover:opacity-75"
activeProps={{ className: 'font-bold underline' }}
- // see styles.css for 'warp' transition
- viewTransition={{ types: ['warp'] }}
+ // see styles.css for 'warp' and 'warp-backwards' transition
+ viewTransition={{
+ types: ({ fromLocation, toLocation }) => {
+ const fromRoute = router
+ .matchRoutes(fromLocation?.pathname ?? '/')
+ .find((entry) => entry.routeId === '/posts/$postId')
+ const toRoute = router
+ .matchRoutes(toLocation?.pathname ?? '/')
+ .find((entry) => entry.routeId === '/posts/$postId')
+
+ const fromIndex = Number(fromRoute?.params.postId)
+ const toIndex = Number(toRoute?.params.postId)
+
+ if (
+ Number.isNaN(fromIndex) ||
+ Number.isNaN(toIndex) ||
+ fromIndex === toIndex
+ ) {
+ return false // no transition
+ }
+
+ return fromIndex > toIndex ? ['warp-backwards'] : ['warp']
+ },
+ }}
>
{post.title.substring(0, 20)}
diff --git a/examples/react/view-transitions/src/styles.css b/examples/react/view-transitions/src/styles.css
index b51e6e1244..502642b383 100644
--- a/examples/react/view-transitions/src/styles.css
+++ b/examples/react/view-transitions/src/styles.css
@@ -87,6 +87,16 @@ html:active-view-transition-type(warp) {
}
}
+html:active-view-transition-type(warp-backwards) {
+ &::view-transition-old(post) {
+ animation: 400ms ease-out both warp-out-backwards;
+ }
+
+ &::view-transition-new(post) {
+ animation: 400ms ease-out both warp-in-backwards;
+ }
+}
+
@keyframes warp-out {
from {
opacity: 1;
@@ -112,3 +122,29 @@ html:active-view-transition-type(warp) {
transform: scale(1) rotate(0deg);
}
}
+
+@keyframes warp-in-backwards {
+ from {
+ opacity: 0;
+ filter: blur(15px) brightness(1.8);
+ transform: scale(0.9) rotate(45deg);
+ }
+ to {
+ opacity: 1;
+ filter: blur(0) brightness(1);
+ transform: scale(1) rotate(0deg);
+ }
+}
+
+@keyframes warp-out-backwards {
+ from {
+ opacity: 1;
+ filter: blur(0) brightness(1);
+ transform: scale(1) rotate(0deg);
+ }
+ to {
+ opacity: 0;
+ filter: blur(15px) brightness(1.8);
+ transform: scale(1.1) rotate(-90deg);
+ }
+}
diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json
index c4d861b689..cbd7e1e24f 100644
--- a/examples/react/with-framer-motion/package.json
+++ b/examples/react/with-framer-motion/package.json
@@ -10,8 +10,8 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/react-router": "^1.170.17",
- "@tanstack/react-router-devtools": "^1.167.0",
+ "@tanstack/react-router": "^1.170.19",
+ "@tanstack/react-router-devtools": "^1.167.1",
"framer-motion": "^11.18.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -23,7 +23,8 @@
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14"
}
}
diff --git a/examples/react/with-trpc-react-query/package.json b/examples/react/with-trpc-react-query/package.json
index 48e7eaa6cb..9e62320366 100644
--- a/examples/react/with-trpc-react-query/package.json
+++ b/examples/react/with-trpc-react-query/package.json
@@ -13,9 +13,9 @@
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.90.0",
"@tanstack/react-query-devtools": "^5.90.0",
- "@tanstack/react-router": "^1.170.17",
- "@tanstack/react-router-devtools": "^1.167.0",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/react-router": "^1.170.19",
+ "@tanstack/react-router-devtools": "^1.167.1",
+ "@tanstack/router-plugin": "^1.168.24",
"@trpc/client": "^11.4.3",
"@trpc/server": "^11.4.3",
"@trpc/tanstack-react-query": "^11.4.3",
diff --git a/examples/react/with-trpc-react-query/src/routeTree.gen.ts b/examples/react/with-trpc-react-query/src/routeTree.gen.ts
index b407b58e4c..548a4b9e88 100644
--- a/examples/react/with-trpc-react-query/src/routeTree.gen.ts
+++ b/examples/react/with-trpc-react-query/src/routeTree.gen.ts
@@ -9,23 +9,23 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as DashboardIndexRouteImport } from './routes/dashboard.index'
import { Route as DashboardPostsRouteImport } from './routes/dashboard.posts'
import { Route as DashboardPostsIndexRouteImport } from './routes/dashboard.posts.index'
import { Route as DashboardPostsPostIdRouteImport } from './routes/dashboard.posts.$postId'
-const DashboardRoute = DashboardRouteImport.update({
- id: '/dashboard',
- path: '/dashboard',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const DashboardRoute = DashboardRouteImport.update({
+ id: '/dashboard',
+ path: '/dashboard',
+ getParentRoute: () => rootRouteImport,
+} as any)
const DashboardIndexRoute = DashboardIndexRouteImport.update({
id: '/',
path: '/',
@@ -98,13 +98,6 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
- '/dashboard': {
- id: '/dashboard'
- path: '/dashboard'
- fullPath: '/dashboard'
- preLoaderRoute: typeof DashboardRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -112,6 +105,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/dashboard': {
+ id: '/dashboard'
+ path: '/dashboard'
+ fullPath: '/dashboard'
+ preLoaderRoute: typeof DashboardRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/dashboard/': {
id: '/dashboard/'
path: '/'
diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json
index 3474e12a6b..6b87233386 100644
--- a/examples/react/with-trpc/package.json
+++ b/examples/react/with-trpc/package.json
@@ -11,9 +11,9 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/react-router": "^1.170.17",
- "@tanstack/react-router-devtools": "^1.167.0",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/react-router": "^1.170.19",
+ "@tanstack/react-router-devtools": "^1.167.1",
+ "@tanstack/router-plugin": "^1.168.24",
"@trpc/client": "^11.4.3",
"@trpc/server": "^11.4.3",
"express": "^5.2.1",
diff --git a/examples/react/with-trpc/src/routeTree.gen.ts b/examples/react/with-trpc/src/routeTree.gen.ts
index b407b58e4c..548a4b9e88 100644
--- a/examples/react/with-trpc/src/routeTree.gen.ts
+++ b/examples/react/with-trpc/src/routeTree.gen.ts
@@ -9,23 +9,23 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as DashboardIndexRouteImport } from './routes/dashboard.index'
import { Route as DashboardPostsRouteImport } from './routes/dashboard.posts'
import { Route as DashboardPostsIndexRouteImport } from './routes/dashboard.posts.index'
import { Route as DashboardPostsPostIdRouteImport } from './routes/dashboard.posts.$postId'
-const DashboardRoute = DashboardRouteImport.update({
- id: '/dashboard',
- path: '/dashboard',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const DashboardRoute = DashboardRouteImport.update({
+ id: '/dashboard',
+ path: '/dashboard',
+ getParentRoute: () => rootRouteImport,
+} as any)
const DashboardIndexRoute = DashboardIndexRouteImport.update({
id: '/',
path: '/',
@@ -98,13 +98,6 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
- '/dashboard': {
- id: '/dashboard'
- path: '/dashboard'
- fullPath: '/dashboard'
- preLoaderRoute: typeof DashboardRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -112,6 +105,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/dashboard': {
+ id: '/dashboard'
+ path: '/dashboard'
+ fullPath: '/dashboard'
+ preLoaderRoute: typeof DashboardRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/dashboard/': {
id: '/dashboard/'
path: '/'
diff --git a/examples/solid/authenticated-routes-firebase/package.json b/examples/solid/authenticated-routes-firebase/package.json
index e93521ece2..0883da702c 100644
--- a/examples/solid/authenticated-routes-firebase/package.json
+++ b/examples/solid/authenticated-routes-firebase/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"firebase": "^11.4.0",
@@ -22,7 +22,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/authenticated-routes-firebase/src/routeTree.gen.ts b/examples/solid/authenticated-routes-firebase/src/routeTree.gen.ts
index ff0ae58ad7..4b98258aea 100644
--- a/examples/solid/authenticated-routes-firebase/src/routeTree.gen.ts
+++ b/examples/solid/authenticated-routes-firebase/src/routeTree.gen.ts
@@ -9,38 +9,38 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as LoginRouteImport } from './routes/login'
-import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as AuthInvoicesRouteImport } from './routes/_auth.invoices'
+import { Route as AuthRouteImport } from './routes/_auth'
+import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthDashboardRouteImport } from './routes/_auth.dashboard'
+import { Route as AuthInvoicesRouteImport } from './routes/_auth.invoices'
import { Route as AuthInvoicesIndexRouteImport } from './routes/_auth.invoices.index'
import { Route as AuthInvoicesInvoiceIdRouteImport } from './routes/_auth.invoices.$invoiceId'
-const LoginRoute = LoginRouteImport.update({
- id: '/login',
- path: '/login',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const LoginRoute = LoginRouteImport.update({
+ id: '/login',
+ path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
-const AuthInvoicesRoute = AuthInvoicesRouteImport.update({
- id: '/invoices',
- path: '/invoices',
- getParentRoute: () => AuthRoute,
-} as any)
const AuthDashboardRoute = AuthDashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => AuthRoute,
} as any)
+const AuthInvoicesRoute = AuthInvoicesRouteImport.update({
+ id: '/invoices',
+ path: '/invoices',
+ getParentRoute: () => AuthRoute,
+} as any)
const AuthInvoicesIndexRoute = AuthInvoicesIndexRouteImport.update({
id: '/',
path: '/',
@@ -107,11 +107,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/login': {
- id: '/login'
- path: '/login'
- fullPath: '/login'
- preLoaderRoute: typeof LoginRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_auth': {
@@ -121,20 +121,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AuthRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/login': {
+ id: '/login'
+ path: '/login'
+ fullPath: '/login'
+ preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
- '/_auth/invoices': {
- id: '/_auth/invoices'
- path: '/invoices'
- fullPath: '/invoices'
- preLoaderRoute: typeof AuthInvoicesRouteImport
- parentRoute: typeof AuthRoute
- }
'/_auth/dashboard': {
id: '/_auth/dashboard'
path: '/dashboard'
@@ -142,6 +135,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AuthDashboardRouteImport
parentRoute: typeof AuthRoute
}
+ '/_auth/invoices': {
+ id: '/_auth/invoices'
+ path: '/invoices'
+ fullPath: '/invoices'
+ preLoaderRoute: typeof AuthInvoicesRouteImport
+ parentRoute: typeof AuthRoute
+ }
'/_auth/invoices/': {
id: '/_auth/invoices/'
path: '/'
diff --git a/examples/solid/authenticated-routes/package.json b/examples/solid/authenticated-routes/package.json
index 58e7f08ed5..3f2f83dce9 100644
--- a/examples/solid/authenticated-routes/package.json
+++ b/examples/solid/authenticated-routes/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"redaxios": "^0.5.1",
@@ -20,7 +20,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/authenticated-routes/src/routeTree.gen.ts b/examples/solid/authenticated-routes/src/routeTree.gen.ts
index ff0ae58ad7..4b98258aea 100644
--- a/examples/solid/authenticated-routes/src/routeTree.gen.ts
+++ b/examples/solid/authenticated-routes/src/routeTree.gen.ts
@@ -9,38 +9,38 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as LoginRouteImport } from './routes/login'
-import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as AuthInvoicesRouteImport } from './routes/_auth.invoices'
+import { Route as AuthRouteImport } from './routes/_auth'
+import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthDashboardRouteImport } from './routes/_auth.dashboard'
+import { Route as AuthInvoicesRouteImport } from './routes/_auth.invoices'
import { Route as AuthInvoicesIndexRouteImport } from './routes/_auth.invoices.index'
import { Route as AuthInvoicesInvoiceIdRouteImport } from './routes/_auth.invoices.$invoiceId'
-const LoginRoute = LoginRouteImport.update({
- id: '/login',
- path: '/login',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const LoginRoute = LoginRouteImport.update({
+ id: '/login',
+ path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
-const AuthInvoicesRoute = AuthInvoicesRouteImport.update({
- id: '/invoices',
- path: '/invoices',
- getParentRoute: () => AuthRoute,
-} as any)
const AuthDashboardRoute = AuthDashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => AuthRoute,
} as any)
+const AuthInvoicesRoute = AuthInvoicesRouteImport.update({
+ id: '/invoices',
+ path: '/invoices',
+ getParentRoute: () => AuthRoute,
+} as any)
const AuthInvoicesIndexRoute = AuthInvoicesIndexRouteImport.update({
id: '/',
path: '/',
@@ -107,11 +107,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/login': {
- id: '/login'
- path: '/login'
- fullPath: '/login'
- preLoaderRoute: typeof LoginRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_auth': {
@@ -121,20 +121,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AuthRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/login': {
+ id: '/login'
+ path: '/login'
+ fullPath: '/login'
+ preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
- '/_auth/invoices': {
- id: '/_auth/invoices'
- path: '/invoices'
- fullPath: '/invoices'
- preLoaderRoute: typeof AuthInvoicesRouteImport
- parentRoute: typeof AuthRoute
- }
'/_auth/dashboard': {
id: '/_auth/dashboard'
path: '/dashboard'
@@ -142,6 +135,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AuthDashboardRouteImport
parentRoute: typeof AuthRoute
}
+ '/_auth/invoices': {
+ id: '/_auth/invoices'
+ path: '/invoices'
+ fullPath: '/invoices'
+ preLoaderRoute: typeof AuthInvoicesRouteImport
+ parentRoute: typeof AuthRoute
+ }
'/_auth/invoices/': {
id: '/_auth/invoices/'
path: '/'
diff --git a/examples/solid/basic-default-search-params/package.json b/examples/solid/basic-default-search-params/package.json
index 164adc091b..15b634c642 100644
--- a/examples/solid/basic-default-search-params/package.json
+++ b/examples/solid/basic-default-search-params/package.json
@@ -20,7 +20,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-devtools-panel/package.json b/examples/solid/basic-devtools-panel/package.json
index 7ec5b60db5..199a2a6a12 100644
--- a/examples/solid/basic-devtools-panel/package.json
+++ b/examples/solid/basic-devtools-panel/package.json
@@ -18,7 +18,8 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-file-based/package.json b/examples/solid/basic-file-based/package.json
index 6a3d8a1ab1..206c3d013b 100644
--- a/examples/solid/basic-file-based/package.json
+++ b/examples/solid/basic-file-based/package.json
@@ -19,8 +19,9 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@tanstack/router-plugin": "^1.168.19",
- "typescript": "^6.0.2",
+ "@tanstack/router-plugin": "^1.168.24",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-file-based/src/routeTree.gen.ts b/examples/solid/basic-file-based/src/routeTree.gen.ts
index bde5d68567..5942acc907 100644
--- a/examples/solid/basic-file-based/src/routeTree.gen.ts
+++ b/examples/solid/basic-file-based/src/routeTree.gen.ts
@@ -9,29 +9,34 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
@@ -42,10 +47,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
+const PathlessLayoutNestedLayoutRouteARoute =
+ PathlessLayoutNestedLayoutRouteARouteImport.update({
+ id: '/route-a',
+ path: '/route-a',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
const PathlessLayoutNestedLayoutRouteBRoute =
PathlessLayoutNestedLayoutRouteBRouteImport.update({
@@ -53,12 +59,6 @@ const PathlessLayoutNestedLayoutRouteBRoute =
path: '/route-b',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteARoute =
- PathlessLayoutNestedLayoutRouteARouteImport.update({
- id: '/route-a',
- path: '/route-a',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -117,11 +117,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_pathlessLayout': {
@@ -131,13 +131,20 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
+ }
'/posts/': {
id: '/posts/'
path: '/'
@@ -152,12 +159,12 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -166,13 +173,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
- }
}
}
diff --git a/examples/solid/basic-non-nested-devtools/package.json b/examples/solid/basic-non-nested-devtools/package.json
index 6aa346e419..c8b89044b8 100644
--- a/examples/solid/basic-non-nested-devtools/package.json
+++ b/examples/solid/basic-non-nested-devtools/package.json
@@ -20,7 +20,8 @@
"devDependencies": {
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-solid-query-file-based/package.json b/examples/solid/basic-solid-query-file-based/package.json
index 780c55ea13..b76242fb06 100644
--- a/examples/solid/basic-solid-query-file-based/package.json
+++ b/examples/solid/basic-solid-query-file-based/package.json
@@ -22,8 +22,9 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@tanstack/router-plugin": "^1.168.19",
- "typescript": "^6.0.2",
+ "@tanstack/router-plugin": "^1.168.24",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-solid-query-file-based/src/routeTree.gen.ts b/examples/solid/basic-solid-query-file-based/src/routeTree.gen.ts
index bde5d68567..5942acc907 100644
--- a/examples/solid/basic-solid-query-file-based/src/routeTree.gen.ts
+++ b/examples/solid/basic-solid-query-file-based/src/routeTree.gen.ts
@@ -9,29 +9,34 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
@@ -42,10 +47,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
+const PathlessLayoutNestedLayoutRouteARoute =
+ PathlessLayoutNestedLayoutRouteARouteImport.update({
+ id: '/route-a',
+ path: '/route-a',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
const PathlessLayoutNestedLayoutRouteBRoute =
PathlessLayoutNestedLayoutRouteBRouteImport.update({
@@ -53,12 +59,6 @@ const PathlessLayoutNestedLayoutRouteBRoute =
path: '/route-b',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteARoute =
- PathlessLayoutNestedLayoutRouteARouteImport.update({
- id: '/route-a',
- path: '/route-a',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -117,11 +117,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_pathlessLayout': {
@@ -131,13 +131,20 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
+ }
'/posts/': {
id: '/posts/'
path: '/'
@@ -152,12 +159,12 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -166,13 +173,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
- }
}
}
diff --git a/examples/solid/basic-solid-query/package.json b/examples/solid/basic-solid-query/package.json
index 26028e22a7..b51904e234 100644
--- a/examples/solid/basic-solid-query/package.json
+++ b/examples/solid/basic-solid-query/package.json
@@ -20,8 +20,9 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
- "@tanstack/router-plugin": "^1.168.19",
- "typescript": "^6.0.2",
+ "@tanstack/router-plugin": "^1.168.24",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-ssr-file-based/package.json b/examples/solid/basic-ssr-file-based/package.json
index c2f94e16f8..080b9b1c87 100644
--- a/examples/solid/basic-ssr-file-based/package.json
+++ b/examples/solid/basic-ssr-file-based/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"compression": "^1.8.0",
"express": "^5.2.1",
@@ -23,7 +23,8 @@
"devDependencies": {
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"@types/express": "^5.0.6",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-ssr-file-based/src/routeTree.gen.ts b/examples/solid/basic-ssr-file-based/src/routeTree.gen.ts
index 26eb09e67f..fc6a3e4377 100644
--- a/examples/solid/basic-ssr-file-based/src/routeTree.gen.ts
+++ b/examples/solid/basic-ssr-file-based/src/routeTree.gen.ts
@@ -9,12 +9,17 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as ErrorRouteImport } from './routes/error'
import { Route as PostsRouteRouteImport } from './routes/posts/route'
-import { Route as IndexRouteImport } from './routes/index'
import { Route as PostsIndexRouteImport } from './routes/posts/index'
import { Route as PostsPostIdRouteImport } from './routes/posts/$postId'
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
const ErrorRoute = ErrorRouteImport.update({
id: '/error',
path: '/error',
@@ -25,11 +30,6 @@ const PostsRouteRoute = PostsRouteRouteImport.update({
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => rootRouteImport,
-} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
@@ -78,6 +78,13 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/error': {
id: '/error'
path: '/error'
@@ -92,13 +99,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsRouteRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
- }
'/posts/': {
id: '/posts/'
path: '/'
diff --git a/examples/solid/basic-ssr-streaming-file-based/package.json b/examples/solid/basic-ssr-streaming-file-based/package.json
index 51d7d63bae..bf21655416 100644
--- a/examples/solid/basic-ssr-streaming-file-based/package.json
+++ b/examples/solid/basic-ssr-streaming-file-based/package.json
@@ -25,9 +25,10 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@types/express": "^5.0.6",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-ssr-streaming-file-based/src/routeTree.gen.ts b/examples/solid/basic-ssr-streaming-file-based/src/routeTree.gen.ts
index be2ed5b18d..0161f550f0 100644
--- a/examples/solid/basic-ssr-streaming-file-based/src/routeTree.gen.ts
+++ b/examples/solid/basic-ssr-streaming-file-based/src/routeTree.gen.ts
@@ -9,19 +9,23 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as CounterRouteImport } from './routes/counter'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as CounterRouteImport } from './routes/counter'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
} as any)
const CounterRoute = CounterRouteImport.update({
@@ -29,15 +33,16 @@ const CounterRoute = CounterRouteImport.update({
path: '/counter',
getParentRoute: () => rootRouteImport,
} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
- getParentRoute: () => rootRouteImport,
-} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
@@ -48,10 +53,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
+const PathlessLayoutNestedLayoutRouteARoute =
+ PathlessLayoutNestedLayoutRouteARouteImport.update({
+ id: '/route-a',
+ path: '/route-a',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
const PathlessLayoutNestedLayoutRouteBRoute =
PathlessLayoutNestedLayoutRouteBRouteImport.update({
@@ -59,12 +65,6 @@ const PathlessLayoutNestedLayoutRouteBRoute =
path: '/route-b',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteARoute =
- PathlessLayoutNestedLayoutRouteARouteImport.update({
- id: '/route-a',
- path: '/route-a',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -129,11 +129,18 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRouteImport
}
'/counter': {
@@ -143,19 +150,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof CounterRouteImport
parentRoute: typeof rootRouteImport
}
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
}
'/posts/': {
id: '/posts/'
@@ -171,12 +178,12 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -185,13 +192,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
- }
}
}
diff --git a/examples/solid/basic-virtual-file-based/package.json b/examples/solid/basic-virtual-file-based/package.json
index 7b60756999..d4db7b88a9 100644
--- a/examples/solid/basic-virtual-file-based/package.json
+++ b/examples/solid/basic-virtual-file-based/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"@tanstack/virtual-file-routes": "^1.162.0",
@@ -21,7 +21,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-virtual-file-based/src/routeTree.gen.ts b/examples/solid/basic-virtual-file-based/src/routeTree.gen.ts
index cb96aee7ba..c41eda86bf 100644
--- a/examples/solid/basic-virtual-file-based/src/routeTree.gen.ts
+++ b/examples/solid/basic-virtual-file-based/src/routeTree.gen.ts
@@ -9,77 +9,77 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/root'
-import { Route as postsPostsRouteImport } from './routes/posts/posts'
-import { Route as layoutFirstLayoutRouteImport } from './routes/layout/first-layout'
import { Route as homeRouteImport } from './routes/home'
-import { Route as postsPostsDetailRouteImport } from './routes/posts/posts-detail'
+import { Route as layoutFirstLayoutRouteImport } from './routes/layout/first-layout'
+import { Route as postsPostsRouteImport } from './routes/posts/posts'
import { Route as layoutSecondLayoutRouteImport } from './routes/layout/second-layout'
-import { Route as postsPostsHomeRouteImport } from './routes/posts/posts-home'
import { Route as ClassicHelloRouteRouteImport } from './routes/file-based-subtree/hello/route'
+import { Route as postsPostsHomeRouteImport } from './routes/posts/posts-home'
+import { Route as postsPostsDetailRouteImport } from './routes/posts/posts-detail'
import { Route as ClassicHelloIndexRouteImport } from './routes/file-based-subtree/hello/index'
-import { Route as ClassicHelloWorldRouteImport } from './routes/file-based-subtree/hello/world'
import { Route as ClassicHelloUniverseRouteImport } from './routes/file-based-subtree/hello/universe'
-import { Route as bRouteImport } from './routes/b'
+import { Route as ClassicHelloWorldRouteImport } from './routes/file-based-subtree/hello/world'
import { Route as aRouteImport } from './routes/a'
+import { Route as bRouteImport } from './routes/b'
-const postsPostsRoute = postsPostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const homeRoute = homeRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const layoutFirstLayoutRoute = layoutFirstLayoutRouteImport.update({
id: '/_first',
getParentRoute: () => rootRouteImport,
} as any)
-const homeRoute = homeRouteImport.update({
- id: '/',
- path: '/',
+const postsPostsRoute = postsPostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const postsPostsDetailRoute = postsPostsDetailRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => postsPostsRoute,
-} as any)
const layoutSecondLayoutRoute = layoutSecondLayoutRouteImport.update({
id: '/_second-layout',
getParentRoute: () => layoutFirstLayoutRoute,
} as any)
+const ClassicHelloRouteRoute = ClassicHelloRouteRouteImport.update({
+ id: '/classic/hello',
+ path: '/classic/hello',
+ getParentRoute: () => rootRouteImport,
+} as any)
const postsPostsHomeRoute = postsPostsHomeRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => postsPostsRoute,
} as any)
-const ClassicHelloRouteRoute = ClassicHelloRouteRouteImport.update({
- id: '/classic/hello',
- path: '/classic/hello',
- getParentRoute: () => rootRouteImport,
+const postsPostsDetailRoute = postsPostsDetailRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => postsPostsRoute,
} as any)
const ClassicHelloIndexRoute = ClassicHelloIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => ClassicHelloRouteRoute,
} as any)
-const ClassicHelloWorldRoute = ClassicHelloWorldRouteImport.update({
- id: '/world',
- path: '/world',
- getParentRoute: () => ClassicHelloRouteRoute,
-} as any)
const ClassicHelloUniverseRoute = ClassicHelloUniverseRouteImport.update({
id: '/universe',
path: '/universe',
getParentRoute: () => ClassicHelloRouteRoute,
} as any)
-const bRoute = bRouteImport.update({
- id: '/route-without-file/layout-b',
- path: '/route-without-file/layout-b',
- getParentRoute: () => layoutSecondLayoutRoute,
+const ClassicHelloWorldRoute = ClassicHelloWorldRouteImport.update({
+ id: '/world',
+ path: '/world',
+ getParentRoute: () => ClassicHelloRouteRoute,
} as any)
const aRoute = aRouteImport.update({
id: '/route-without-file/layout-a',
path: '/route-without-file/layout-a',
getParentRoute: () => layoutSecondLayoutRoute,
} as any)
+const bRoute = bRouteImport.update({
+ id: '/route-without-file/layout-b',
+ path: '/route-without-file/layout-b',
+ getParentRoute: () => layoutSecondLayoutRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof homeRoute
@@ -166,11 +166,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof postsPostsRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof homeRouteImport
parentRoute: typeof rootRouteImport
}
'/_first': {
@@ -180,20 +180,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof layoutFirstLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof homeRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof postsPostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts/$postId': {
- id: '/posts/$postId'
- path: '/$postId'
- fullPath: '/posts/$postId'
- preLoaderRoute: typeof postsPostsDetailRouteImport
- parentRoute: typeof postsPostsRoute
- }
'/_first/_second-layout': {
id: '/_first/_second-layout'
path: ''
@@ -201,6 +194,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof layoutSecondLayoutRouteImport
parentRoute: typeof layoutFirstLayoutRoute
}
+ '/classic/hello': {
+ id: '/classic/hello'
+ path: '/classic/hello'
+ fullPath: '/classic/hello'
+ preLoaderRoute: typeof ClassicHelloRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/posts/': {
id: '/posts/'
path: '/'
@@ -208,12 +208,12 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof postsPostsHomeRouteImport
parentRoute: typeof postsPostsRoute
}
- '/classic/hello': {
- id: '/classic/hello'
- path: '/classic/hello'
- fullPath: '/classic/hello'
- preLoaderRoute: typeof ClassicHelloRouteRouteImport
- parentRoute: typeof rootRouteImport
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof postsPostsDetailRouteImport
+ parentRoute: typeof postsPostsRoute
}
'/classic/hello/': {
id: '/classic/hello/'
@@ -222,13 +222,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ClassicHelloIndexRouteImport
parentRoute: typeof ClassicHelloRouteRoute
}
- '/classic/hello/world': {
- id: '/classic/hello/world'
- path: '/world'
- fullPath: '/classic/hello/world'
- preLoaderRoute: typeof ClassicHelloWorldRouteImport
- parentRoute: typeof ClassicHelloRouteRoute
- }
'/classic/hello/universe': {
id: '/classic/hello/universe'
path: '/universe'
@@ -236,12 +229,12 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ClassicHelloUniverseRouteImport
parentRoute: typeof ClassicHelloRouteRoute
}
- '/_first/_second-layout/route-without-file/layout-b': {
- id: '/_first/_second-layout/route-without-file/layout-b'
- path: '/route-without-file/layout-b'
- fullPath: '/route-without-file/layout-b'
- preLoaderRoute: typeof bRouteImport
- parentRoute: typeof layoutSecondLayoutRoute
+ '/classic/hello/world': {
+ id: '/classic/hello/world'
+ path: '/world'
+ fullPath: '/classic/hello/world'
+ preLoaderRoute: typeof ClassicHelloWorldRouteImport
+ parentRoute: typeof ClassicHelloRouteRoute
}
'/_first/_second-layout/route-without-file/layout-a': {
id: '/_first/_second-layout/route-without-file/layout-a'
@@ -250,6 +243,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof aRouteImport
parentRoute: typeof layoutSecondLayoutRoute
}
+ '/_first/_second-layout/route-without-file/layout-b': {
+ id: '/_first/_second-layout/route-without-file/layout-b'
+ path: '/route-without-file/layout-b'
+ fullPath: '/route-without-file/layout-b'
+ preLoaderRoute: typeof bRouteImport
+ parentRoute: typeof layoutSecondLayoutRoute
+ }
}
}
diff --git a/examples/solid/basic-virtual-inside-file-based/package.json b/examples/solid/basic-virtual-inside-file-based/package.json
index 5de13a45c1..2fdb2138e3 100644
--- a/examples/solid/basic-virtual-inside-file-based/package.json
+++ b/examples/solid/basic-virtual-inside-file-based/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"@tanstack/virtual-file-routes": "^1.162.0",
@@ -21,7 +21,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/basic-virtual-inside-file-based/src/routeTree.gen.ts b/examples/solid/basic-virtual-inside-file-based/src/routeTree.gen.ts
index 037dac78a3..6117330aa3 100644
--- a/examples/solid/basic-virtual-inside-file-based/src/routeTree.gen.ts
+++ b/examples/solid/basic-virtual-inside-file-based/src/routeTree.gen.ts
@@ -9,36 +9,31 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as PostsDetailsRouteImport } from './routes/posts/details'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as PostsRouteImport } from './routes/posts'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as PostsHomeRouteImport } from './routes/posts/home'
-import { Route as PostsLetsGoIndexRouteImport } from './routes/posts/lets-go/index'
-import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as PostsDetailsRouteImport } from './routes/posts/details'
import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as PostsLetsGoIndexRouteImport } from './routes/posts/lets-go/index'
import { Route as PostsLetsGoDeeperHomeRouteImport } from './routes/posts/lets-go/deeper/home'
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const PostsDetailsRoute = PostsDetailsRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => PostsRoute,
-} as any)
const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
@@ -48,20 +43,25 @@ const PostsHomeRoute = PostsHomeRouteImport.update({
path: '/',
getParentRoute: () => PostsRoute,
} as any)
-const PostsLetsGoIndexRoute = PostsLetsGoIndexRouteImport.update({
- id: '/inception/',
- path: '/inception/',
+const PostsDetailsRoute = PostsDetailsRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any)
const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
id: '/layout-b',
path: '/layout-b',
getParentRoute: () => LayoutLayout2Route,
} as any)
-const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
- id: '/layout-a',
- path: '/layout-a',
- getParentRoute: () => LayoutLayout2Route,
+const PostsLetsGoIndexRoute = PostsLetsGoIndexRouteImport.update({
+ id: '/inception/',
+ path: '/inception/',
+ getParentRoute: () => PostsRoute,
} as any)
const PostsLetsGoDeeperHomeRoute = PostsLetsGoDeeperHomeRouteImport.update({
id: '/inception/deeper/',
@@ -143,11 +143,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_layout': {
@@ -157,20 +157,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts/$postId': {
- id: '/posts/$postId'
- path: '/$postId'
- fullPath: '/posts/$postId'
- preLoaderRoute: typeof PostsDetailsRouteImport
- parentRoute: typeof PostsRoute
- }
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
@@ -185,13 +178,20 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsHomeRouteImport
parentRoute: typeof PostsRoute
}
- '/posts/inception/': {
- id: '/posts/inception/'
- path: '/inception'
- fullPath: '/posts/inception/'
- preLoaderRoute: typeof PostsLetsGoIndexRouteImport
+ '/posts/$postId': {
+ id: '/posts/$postId'
+ path: '/$postId'
+ fullPath: '/posts/$postId'
+ preLoaderRoute: typeof PostsDetailsRouteImport
parentRoute: typeof PostsRoute
}
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
+ }
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
path: '/layout-b'
@@ -199,12 +199,12 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
parentRoute: typeof LayoutLayout2Route
}
- '/_layout/_layout-2/layout-a': {
- id: '/_layout/_layout-2/layout-a'
- path: '/layout-a'
- fullPath: '/layout-a'
- preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
- parentRoute: typeof LayoutLayout2Route
+ '/posts/inception/': {
+ id: '/posts/inception/'
+ path: '/inception'
+ fullPath: '/posts/inception/'
+ preLoaderRoute: typeof PostsLetsGoIndexRouteImport
+ parentRoute: typeof PostsRoute
}
'/posts/inception/deeper/': {
id: '/posts/inception/deeper/'
diff --git a/examples/solid/basic/package.json b/examples/solid/basic/package.json
index 211949d7f7..2c4b859f88 100644
--- a/examples/solid/basic/package.json
+++ b/examples/solid/basic/package.json
@@ -20,7 +20,8 @@
"devDependencies": {
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/deferred-data/package.json b/examples/solid/deferred-data/package.json
index f6ca477664..6d9035bb11 100644
--- a/examples/solid/deferred-data/package.json
+++ b/examples/solid/deferred-data/package.json
@@ -19,7 +19,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/i18n-paraglide/package.json b/examples/solid/i18n-paraglide/package.json
index b054fca184..122a205b35 100644
--- a/examples/solid/i18n-paraglide/package.json
+++ b/examples/solid/i18n-paraglide/package.json
@@ -12,7 +12,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"solid-js": "2.0.0-beta.29",
"tailwindcss": "^4.2.2"
@@ -20,7 +20,8 @@
"devDependencies": {
"@inlang/paraglide-js": "^2.4.0",
"@types/node": "^22.18.6",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/i18n-paraglide/src/routeTree.gen.ts b/examples/solid/i18n-paraglide/src/routeTree.gen.ts
index 333a815c38..98df1e83d8 100644
--- a/examples/solid/i18n-paraglide/src/routeTree.gen.ts
+++ b/examples/solid/i18n-paraglide/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AboutRouteImport } from './routes/about'
-const AboutRoute = AboutRouteImport.update({
- id: '/about',
- path: '/about',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/about': {
- id: '/about'
- path: '/about'
- fullPath: '/about'
- preLoaderRoute: typeof AboutRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/kitchen-sink-file-based/package.json b/examples/solid/kitchen-sink-file-based/package.json
index 800187b902..12ab856cb9 100644
--- a/examples/solid/kitchen-sink-file-based/package.json
+++ b/examples/solid/kitchen-sink-file-based/package.json
@@ -20,8 +20,9 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@tanstack/router-plugin": "^1.168.19",
- "typescript": "^6.0.2",
+ "@tanstack/router-plugin": "^1.168.24",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/kitchen-sink-file-based/src/routeTree.gen.ts b/examples/solid/kitchen-sink-file-based/src/routeTree.gen.ts
index 0c6e04f63d..76bb314a71 100644
--- a/examples/solid/kitchen-sink-file-based/src/routeTree.gen.ts
+++ b/examples/solid/kitchen-sink-file-based/src/routeTree.gen.ts
@@ -9,81 +9,71 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as LoginRouteImport } from './routes/login'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthRouteImport } from './routes/_auth'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as DashboardRouteRouteImport } from './routes/dashboard.route'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as ExpensiveIndexRouteImport } from './routes/expensive/index'
-import { Route as DashboardIndexRouteImport } from './routes/dashboard.index'
-import { Route as PathlessLayoutRouteBRouteImport } from './routes/_pathlessLayout.route-b'
-import { Route as PathlessLayoutRouteARouteImport } from './routes/_pathlessLayout.route-a'
-import { Route as AuthProfileRouteImport } from './routes/_auth.profile'
+import { Route as LoginRouteImport } from './routes/login'
import { Route as thisFolderIsNotInTheUrlRouteGroupRouteImport } from './routes/(this-folder-is-not-in-the-url)/route-group'
-import { Route as DashboardUsersRouteRouteImport } from './routes/dashboard.users.route'
+import { Route as AuthProfileRouteImport } from './routes/_auth.profile'
+import { Route as PathlessLayoutRouteARouteImport } from './routes/_pathlessLayout.route-a'
+import { Route as PathlessLayoutRouteBRouteImport } from './routes/_pathlessLayout.route-b'
+import { Route as DashboardIndexRouteImport } from './routes/dashboard.index'
import { Route as DashboardInvoicesRouteRouteImport } from './routes/dashboard.invoices.route'
-import { Route as DashboardUsersIndexRouteImport } from './routes/dashboard.users.index'
+import { Route as DashboardUsersRouteRouteImport } from './routes/dashboard.users.route'
+import { Route as ExpensiveIndexRouteImport } from './routes/expensive/index'
import { Route as DashboardInvoicesIndexRouteImport } from './routes/dashboard.invoices.index'
-import { Route as DashboardUsersUserRouteImport } from './routes/dashboard.users.user'
import { Route as DashboardInvoicesInvoiceIdRouteImport } from './routes/dashboard.invoices.$invoiceId'
+import { Route as DashboardUsersIndexRouteImport } from './routes/dashboard.users.index'
+import { Route as DashboardUsersUserRouteImport } from './routes/dashboard.users.user'
-const LoginRoute = LoginRouteImport.update({
- id: '/login',
- path: '/login',
- getParentRoute: () => rootRouteImport,
-} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
getParentRoute: () => rootRouteImport,
} as any)
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
+ getParentRoute: () => rootRouteImport,
+} as any)
const DashboardRouteRoute = DashboardRouteRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => rootRouteImport,
-} as any)
-const ExpensiveIndexRoute = ExpensiveIndexRouteImport.update({
- id: '/expensive/',
- path: '/expensive/',
+const LoginRoute = LoginRouteImport.update({
+ id: '/login',
+ path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
-const DashboardIndexRoute = DashboardIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => DashboardRouteRoute,
-} as any)
-const PathlessLayoutRouteBRoute = PathlessLayoutRouteBRouteImport.update({
- id: '/route-b',
- path: '/route-b',
- getParentRoute: () => PathlessLayoutRoute,
+const thisFolderIsNotInTheUrlRouteGroupRoute =
+ thisFolderIsNotInTheUrlRouteGroupRouteImport.update({
+ id: '/(this-folder-is-not-in-the-url)/route-group',
+ path: '/route-group',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const AuthProfileRoute = AuthProfileRouteImport.update({
+ id: '/profile',
+ path: '/profile',
+ getParentRoute: () => AuthRoute,
} as any)
const PathlessLayoutRouteARoute = PathlessLayoutRouteARouteImport.update({
id: '/route-a',
path: '/route-a',
getParentRoute: () => PathlessLayoutRoute,
} as any)
-const AuthProfileRoute = AuthProfileRouteImport.update({
- id: '/profile',
- path: '/profile',
- getParentRoute: () => AuthRoute,
+const PathlessLayoutRouteBRoute = PathlessLayoutRouteBRouteImport.update({
+ id: '/route-b',
+ path: '/route-b',
+ getParentRoute: () => PathlessLayoutRoute,
} as any)
-const thisFolderIsNotInTheUrlRouteGroupRoute =
- thisFolderIsNotInTheUrlRouteGroupRouteImport.update({
- id: '/(this-folder-is-not-in-the-url)/route-group',
- path: '/route-group',
- getParentRoute: () => rootRouteImport,
- } as any)
-const DashboardUsersRouteRoute = DashboardUsersRouteRouteImport.update({
- id: '/users',
- path: '/users',
+const DashboardIndexRoute = DashboardIndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardInvoicesRouteRoute = DashboardInvoicesRouteRouteImport.update({
@@ -91,27 +81,37 @@ const DashboardInvoicesRouteRoute = DashboardInvoicesRouteRouteImport.update({
path: '/invoices',
getParentRoute: () => DashboardRouteRoute,
} as any)
-const DashboardUsersIndexRoute = DashboardUsersIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => DashboardUsersRouteRoute,
+const DashboardUsersRouteRoute = DashboardUsersRouteRouteImport.update({
+ id: '/users',
+ path: '/users',
+ getParentRoute: () => DashboardRouteRoute,
+} as any)
+const ExpensiveIndexRoute = ExpensiveIndexRouteImport.update({
+ id: '/expensive/',
+ path: '/expensive/',
+ getParentRoute: () => rootRouteImport,
} as any)
const DashboardInvoicesIndexRoute = DashboardInvoicesIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => DashboardInvoicesRouteRoute,
} as any)
-const DashboardUsersUserRoute = DashboardUsersUserRouteImport.update({
- id: '/user',
- path: '/user',
- getParentRoute: () => DashboardUsersRouteRoute,
-} as any)
const DashboardInvoicesInvoiceIdRoute =
DashboardInvoicesInvoiceIdRouteImport.update({
id: '/$invoiceId',
path: '/$invoiceId',
getParentRoute: () => DashboardInvoicesRouteRoute,
} as any)
+const DashboardUsersIndexRoute = DashboardUsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => DashboardUsersRouteRoute,
+} as any)
+const DashboardUsersUserRoute = DashboardUsersUserRouteImport.update({
+ id: '/user',
+ path: '/user',
+ getParentRoute: () => DashboardUsersRouteRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -229,18 +229,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/login': {
- id: '/login'
- path: '/login'
- fullPath: '/login'
- preLoaderRoute: typeof LoginRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
+ '/': {
+ id: '/'
+ path: '/'
fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_auth': {
@@ -250,6 +243,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AuthRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
@@ -257,33 +257,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardRouteRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/login': {
+ id: '/login'
+ path: '/login'
+ fullPath: '/login'
+ preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
- '/expensive/': {
- id: '/expensive/'
- path: '/expensive'
- fullPath: '/expensive/'
- preLoaderRoute: typeof ExpensiveIndexRouteImport
+ '/(this-folder-is-not-in-the-url)/route-group': {
+ id: '/(this-folder-is-not-in-the-url)/route-group'
+ path: '/route-group'
+ fullPath: '/route-group'
+ preLoaderRoute: typeof thisFolderIsNotInTheUrlRouteGroupRouteImport
parentRoute: typeof rootRouteImport
}
- '/dashboard/': {
- id: '/dashboard/'
- path: '/'
- fullPath: '/dashboard/'
- preLoaderRoute: typeof DashboardIndexRouteImport
- parentRoute: typeof DashboardRouteRoute
- }
- '/_pathlessLayout/route-b': {
- id: '/_pathlessLayout/route-b'
- path: '/route-b'
- fullPath: '/route-b'
- preLoaderRoute: typeof PathlessLayoutRouteBRouteImport
- parentRoute: typeof PathlessLayoutRoute
+ '/_auth/profile': {
+ id: '/_auth/profile'
+ path: '/profile'
+ fullPath: '/profile'
+ preLoaderRoute: typeof AuthProfileRouteImport
+ parentRoute: typeof AuthRoute
}
'/_pathlessLayout/route-a': {
id: '/_pathlessLayout/route-a'
@@ -292,25 +285,18 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutRouteARouteImport
parentRoute: typeof PathlessLayoutRoute
}
- '/_auth/profile': {
- id: '/_auth/profile'
- path: '/profile'
- fullPath: '/profile'
- preLoaderRoute: typeof AuthProfileRouteImport
- parentRoute: typeof AuthRoute
- }
- '/(this-folder-is-not-in-the-url)/route-group': {
- id: '/(this-folder-is-not-in-the-url)/route-group'
- path: '/route-group'
- fullPath: '/route-group'
- preLoaderRoute: typeof thisFolderIsNotInTheUrlRouteGroupRouteImport
- parentRoute: typeof rootRouteImport
+ '/_pathlessLayout/route-b': {
+ id: '/_pathlessLayout/route-b'
+ path: '/route-b'
+ fullPath: '/route-b'
+ preLoaderRoute: typeof PathlessLayoutRouteBRouteImport
+ parentRoute: typeof PathlessLayoutRoute
}
- '/dashboard/users': {
- id: '/dashboard/users'
- path: '/users'
- fullPath: '/dashboard/users'
- preLoaderRoute: typeof DashboardUsersRouteRouteImport
+ '/dashboard/': {
+ id: '/dashboard/'
+ path: '/'
+ fullPath: '/dashboard/'
+ preLoaderRoute: typeof DashboardIndexRouteImport
parentRoute: typeof DashboardRouteRoute
}
'/dashboard/invoices': {
@@ -320,12 +306,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardInvoicesRouteRouteImport
parentRoute: typeof DashboardRouteRoute
}
- '/dashboard/users/': {
- id: '/dashboard/users/'
- path: '/'
- fullPath: '/dashboard/users/'
- preLoaderRoute: typeof DashboardUsersIndexRouteImport
- parentRoute: typeof DashboardUsersRouteRoute
+ '/dashboard/users': {
+ id: '/dashboard/users'
+ path: '/users'
+ fullPath: '/dashboard/users'
+ preLoaderRoute: typeof DashboardUsersRouteRouteImport
+ parentRoute: typeof DashboardRouteRoute
+ }
+ '/expensive/': {
+ id: '/expensive/'
+ path: '/expensive'
+ fullPath: '/expensive/'
+ preLoaderRoute: typeof ExpensiveIndexRouteImport
+ parentRoute: typeof rootRouteImport
}
'/dashboard/invoices/': {
id: '/dashboard/invoices/'
@@ -334,13 +327,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardInvoicesIndexRouteImport
parentRoute: typeof DashboardInvoicesRouteRoute
}
- '/dashboard/users/user': {
- id: '/dashboard/users/user'
- path: '/user'
- fullPath: '/dashboard/users/user'
- preLoaderRoute: typeof DashboardUsersUserRouteImport
- parentRoute: typeof DashboardUsersRouteRoute
- }
'/dashboard/invoices/$invoiceId': {
id: '/dashboard/invoices/$invoiceId'
path: '/$invoiceId'
@@ -348,6 +334,20 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardInvoicesInvoiceIdRouteImport
parentRoute: typeof DashboardInvoicesRouteRoute
}
+ '/dashboard/users/': {
+ id: '/dashboard/users/'
+ path: '/'
+ fullPath: '/dashboard/users/'
+ preLoaderRoute: typeof DashboardUsersIndexRouteImport
+ parentRoute: typeof DashboardUsersRouteRoute
+ }
+ '/dashboard/users/user': {
+ id: '/dashboard/users/user'
+ path: '/user'
+ fullPath: '/dashboard/users/user'
+ preLoaderRoute: typeof DashboardUsersUserRouteImport
+ parentRoute: typeof DashboardUsersRouteRoute
+ }
}
}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/package.json b/examples/solid/kitchen-sink-solid-query-file-based/package.json
index e810fd1edf..2e22e82920 100644
--- a/examples/solid/kitchen-sink-solid-query-file-based/package.json
+++ b/examples/solid/kitchen-sink-solid-query-file-based/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-query": "^6.0.0-beta.7",
"@tanstack/solid-query-devtools": "^6.0.0-beta.7",
"@tanstack/solid-router": "^2.0.0-beta.29",
@@ -23,7 +23,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/kitchen-sink-solid-query-file-based/src/routeTree.gen.ts b/examples/solid/kitchen-sink-solid-query-file-based/src/routeTree.gen.ts
index e296846127..8235344782 100644
--- a/examples/solid/kitchen-sink-solid-query-file-based/src/routeTree.gen.ts
+++ b/examples/solid/kitchen-sink-solid-query-file-based/src/routeTree.gen.ts
@@ -9,108 +9,108 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as LoginRouteImport } from './routes/login'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthRouteImport } from './routes/_auth'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as DashboardRouteRouteImport } from './routes/dashboard.route'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as ExpensiveIndexRouteImport } from './routes/expensive/index'
-import { Route as DashboardIndexRouteImport } from './routes/dashboard.index'
-import { Route as FooBarRouteImport } from './routes/foo/bar'
-import { Route as PathlessLayoutRouteBRouteImport } from './routes/_pathlessLayout.route-b'
-import { Route as PathlessLayoutRouteARouteImport } from './routes/_pathlessLayout.route-a'
+import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthProfileRouteImport } from './routes/_auth.profile'
-import { Route as DashboardUsersRouteRouteImport } from './routes/dashboard.users.route'
+import { Route as PathlessLayoutRouteARouteImport } from './routes/_pathlessLayout.route-a'
+import { Route as PathlessLayoutRouteBRouteImport } from './routes/_pathlessLayout.route-b'
+import { Route as DashboardIndexRouteImport } from './routes/dashboard.index'
import { Route as DashboardInvoicesRouteRouteImport } from './routes/dashboard.invoices.route'
-import { Route as DashboardUsersIndexRouteImport } from './routes/dashboard.users.index'
+import { Route as DashboardUsersRouteRouteImport } from './routes/dashboard.users.route'
+import { Route as ExpensiveIndexRouteImport } from './routes/expensive/index'
+import { Route as FooBarRouteImport } from './routes/foo/bar'
import { Route as DashboardInvoicesIndexRouteImport } from './routes/dashboard.invoices.index'
-import { Route as DashboardUsersUserRouteImport } from './routes/dashboard.users.user'
import { Route as DashboardInvoicesInvoiceIdRouteImport } from './routes/dashboard.invoices.$invoiceId'
+import { Route as DashboardUsersIndexRouteImport } from './routes/dashboard.users.index'
+import { Route as DashboardUsersUserRouteImport } from './routes/dashboard.users.user'
-const LoginRoute = LoginRouteImport.update({
- id: '/login',
- path: '/login',
- getParentRoute: () => rootRouteImport,
-} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
getParentRoute: () => rootRouteImport,
} as any)
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
+ getParentRoute: () => rootRouteImport,
+} as any)
const DashboardRouteRoute = DashboardRouteRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => rootRouteImport,
-} as any)
-const ExpensiveIndexRoute = ExpensiveIndexRouteImport.update({
- id: '/expensive/',
- path: '/expensive/',
+const LoginRoute = LoginRouteImport.update({
+ id: '/login',
+ path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
-const DashboardIndexRoute = DashboardIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => DashboardRouteRoute,
+const AuthProfileRoute = AuthProfileRouteImport.update({
+ id: '/profile',
+ path: '/profile',
+ getParentRoute: () => AuthRoute,
} as any)
-const FooBarRoute = FooBarRouteImport.update({
- id: '/foo/bar',
- path: '/foo/bar',
- getParentRoute: () => rootRouteImport,
+const PathlessLayoutRouteARoute = PathlessLayoutRouteARouteImport.update({
+ id: '/route-a',
+ path: '/route-a',
+ getParentRoute: () => PathlessLayoutRoute,
} as any)
const PathlessLayoutRouteBRoute = PathlessLayoutRouteBRouteImport.update({
id: '/route-b',
path: '/route-b',
getParentRoute: () => PathlessLayoutRoute,
} as any)
-const PathlessLayoutRouteARoute = PathlessLayoutRouteARouteImport.update({
- id: '/route-a',
- path: '/route-a',
- getParentRoute: () => PathlessLayoutRoute,
+const DashboardIndexRoute = DashboardIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
-const AuthProfileRoute = AuthProfileRouteImport.update({
- id: '/profile',
- path: '/profile',
- getParentRoute: () => AuthRoute,
+const DashboardInvoicesRouteRoute = DashboardInvoicesRouteRouteImport.update({
+ id: '/invoices',
+ path: '/invoices',
+ getParentRoute: () => DashboardRouteRoute,
} as any)
const DashboardUsersRouteRoute = DashboardUsersRouteRouteImport.update({
id: '/users',
path: '/users',
getParentRoute: () => DashboardRouteRoute,
} as any)
-const DashboardInvoicesRouteRoute = DashboardInvoicesRouteRouteImport.update({
- id: '/invoices',
- path: '/invoices',
- getParentRoute: () => DashboardRouteRoute,
+const ExpensiveIndexRoute = ExpensiveIndexRouteImport.update({
+ id: '/expensive/',
+ path: '/expensive/',
+ getParentRoute: () => rootRouteImport,
} as any)
-const DashboardUsersIndexRoute = DashboardUsersIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => DashboardUsersRouteRoute,
+const FooBarRoute = FooBarRouteImport.update({
+ id: '/foo/bar',
+ path: '/foo/bar',
+ getParentRoute: () => rootRouteImport,
} as any)
const DashboardInvoicesIndexRoute = DashboardInvoicesIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => DashboardInvoicesRouteRoute,
} as any)
-const DashboardUsersUserRoute = DashboardUsersUserRouteImport.update({
- id: '/user',
- path: '/user',
- getParentRoute: () => DashboardUsersRouteRoute,
-} as any)
const DashboardInvoicesInvoiceIdRoute =
DashboardInvoicesInvoiceIdRouteImport.update({
id: '/$invoiceId',
path: '/$invoiceId',
getParentRoute: () => DashboardInvoicesRouteRoute,
} as any)
+const DashboardUsersIndexRoute = DashboardUsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => DashboardUsersRouteRoute,
+} as any)
+const DashboardUsersUserRoute = DashboardUsersUserRouteImport.update({
+ id: '/user',
+ path: '/user',
+ getParentRoute: () => DashboardUsersRouteRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -228,18 +228,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/login': {
- id: '/login'
- path: '/login'
- fullPath: '/login'
- preLoaderRoute: typeof LoginRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
+ '/': {
+ id: '/'
+ path: '/'
fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_auth': {
@@ -249,6 +242,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AuthRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/dashboard': {
id: '/dashboard'
path: '/dashboard'
@@ -256,33 +256,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardRouteRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/expensive/': {
- id: '/expensive/'
- path: '/expensive'
- fullPath: '/expensive/'
- preLoaderRoute: typeof ExpensiveIndexRouteImport
+ '/login': {
+ id: '/login'
+ path: '/login'
+ fullPath: '/login'
+ preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
- '/dashboard/': {
- id: '/dashboard/'
- path: '/'
- fullPath: '/dashboard/'
- preLoaderRoute: typeof DashboardIndexRouteImport
- parentRoute: typeof DashboardRouteRoute
+ '/_auth/profile': {
+ id: '/_auth/profile'
+ path: '/profile'
+ fullPath: '/profile'
+ preLoaderRoute: typeof AuthProfileRouteImport
+ parentRoute: typeof AuthRoute
}
- '/foo/bar': {
- id: '/foo/bar'
- path: '/foo/bar'
- fullPath: '/foo/bar'
- preLoaderRoute: typeof FooBarRouteImport
- parentRoute: typeof rootRouteImport
+ '/_pathlessLayout/route-a': {
+ id: '/_pathlessLayout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutRoute
}
'/_pathlessLayout/route-b': {
id: '/_pathlessLayout/route-b'
@@ -291,19 +284,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutRoute
}
- '/_pathlessLayout/route-a': {
- id: '/_pathlessLayout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutRoute
+ '/dashboard/': {
+ id: '/dashboard/'
+ path: '/'
+ fullPath: '/dashboard/'
+ preLoaderRoute: typeof DashboardIndexRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
- '/_auth/profile': {
- id: '/_auth/profile'
- path: '/profile'
- fullPath: '/profile'
- preLoaderRoute: typeof AuthProfileRouteImport
- parentRoute: typeof AuthRoute
+ '/dashboard/invoices': {
+ id: '/dashboard/invoices'
+ path: '/invoices'
+ fullPath: '/dashboard/invoices'
+ preLoaderRoute: typeof DashboardInvoicesRouteRouteImport
+ parentRoute: typeof DashboardRouteRoute
}
'/dashboard/users': {
id: '/dashboard/users'
@@ -312,19 +305,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardUsersRouteRouteImport
parentRoute: typeof DashboardRouteRoute
}
- '/dashboard/invoices': {
- id: '/dashboard/invoices'
- path: '/invoices'
- fullPath: '/dashboard/invoices'
- preLoaderRoute: typeof DashboardInvoicesRouteRouteImport
- parentRoute: typeof DashboardRouteRoute
+ '/expensive/': {
+ id: '/expensive/'
+ path: '/expensive'
+ fullPath: '/expensive/'
+ preLoaderRoute: typeof ExpensiveIndexRouteImport
+ parentRoute: typeof rootRouteImport
}
- '/dashboard/users/': {
- id: '/dashboard/users/'
- path: '/'
- fullPath: '/dashboard/users/'
- preLoaderRoute: typeof DashboardUsersIndexRouteImport
- parentRoute: typeof DashboardUsersRouteRoute
+ '/foo/bar': {
+ id: '/foo/bar'
+ path: '/foo/bar'
+ fullPath: '/foo/bar'
+ preLoaderRoute: typeof FooBarRouteImport
+ parentRoute: typeof rootRouteImport
}
'/dashboard/invoices/': {
id: '/dashboard/invoices/'
@@ -333,13 +326,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardInvoicesIndexRouteImport
parentRoute: typeof DashboardInvoicesRouteRoute
}
- '/dashboard/users/user': {
- id: '/dashboard/users/user'
- path: '/user'
- fullPath: '/dashboard/users/user'
- preLoaderRoute: typeof DashboardUsersUserRouteImport
- parentRoute: typeof DashboardUsersRouteRoute
- }
'/dashboard/invoices/$invoiceId': {
id: '/dashboard/invoices/$invoiceId'
path: '/$invoiceId'
@@ -347,6 +333,20 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DashboardInvoicesInvoiceIdRouteImport
parentRoute: typeof DashboardInvoicesRouteRoute
}
+ '/dashboard/users/': {
+ id: '/dashboard/users/'
+ path: '/'
+ fullPath: '/dashboard/users/'
+ preLoaderRoute: typeof DashboardUsersIndexRouteImport
+ parentRoute: typeof DashboardUsersRouteRoute
+ }
+ '/dashboard/users/user': {
+ id: '/dashboard/users/user'
+ path: '/user'
+ fullPath: '/dashboard/users/user'
+ preLoaderRoute: typeof DashboardUsersUserRouteImport
+ parentRoute: typeof DashboardUsersRouteRoute
+ }
}
}
diff --git a/examples/solid/kitchen-sink-solid-query/package.json b/examples/solid/kitchen-sink-solid-query/package.json
index 358f8e41fc..7af06c4f44 100644
--- a/examples/solid/kitchen-sink-solid-query/package.json
+++ b/examples/solid/kitchen-sink-solid-query/package.json
@@ -22,7 +22,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/kitchen-sink/package.json b/examples/solid/kitchen-sink/package.json
index 00e7fa13ef..74097e94ab 100644
--- a/examples/solid/kitchen-sink/package.json
+++ b/examples/solid/kitchen-sink/package.json
@@ -20,7 +20,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/large-file-based/package.json b/examples/solid/large-file-based/package.json
index 5d3347b36e..60fbe7ea9f 100644
--- a/examples/solid/large-file-based/package.json
+++ b/examples/solid/large-file-based/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-query": "^6.0.0-beta.7",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
@@ -23,7 +23,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/large-file-based/src/routeTree.gen.ts b/examples/solid/large-file-based/src/routeTree.gen.ts
index d345b69acf..ea7329778c 100644
--- a/examples/solid/large-file-based/src/routeTree.gen.ts
+++ b/examples/solid/large-file-based/src/routeTree.gen.ts
@@ -9,23 +9,18 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as RelativeRouteImport } from './routes/relative'
-import { Route as LinkPropsRouteImport } from './routes/linkProps'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as AbsoluteRouteImport } from './routes/absolute'
-import { Route as SearchRouteRouteImport } from './routes/search/route'
+import { Route as LinkPropsRouteImport } from './routes/linkProps'
import { Route as ParamsRouteRouteImport } from './routes/params/route'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as SearchSearchPlaceholderRouteImport } from './routes/search/searchPlaceholder'
+import { Route as RelativeRouteImport } from './routes/relative'
+import { Route as SearchRouteRouteImport } from './routes/search/route'
import { Route as ParamsParamsPlaceholderRouteImport } from './routes/params/$paramsPlaceholder'
+import { Route as SearchSearchPlaceholderRouteImport } from './routes/search/searchPlaceholder'
-const RelativeRoute = RelativeRouteImport.update({
- id: '/relative',
- path: '/relative',
- getParentRoute: () => rootRouteImport,
-} as any)
-const LinkPropsRoute = LinkPropsRouteImport.update({
- id: '/linkProps',
- path: '/linkProps',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AbsoluteRoute = AbsoluteRouteImport.update({
@@ -33,9 +28,9 @@ const AbsoluteRoute = AbsoluteRouteImport.update({
path: '/absolute',
getParentRoute: () => rootRouteImport,
} as any)
-const SearchRouteRoute = SearchRouteRouteImport.update({
- id: '/search',
- path: '/search',
+const LinkPropsRoute = LinkPropsRouteImport.update({
+ id: '/linkProps',
+ path: '/linkProps',
getParentRoute: () => rootRouteImport,
} as any)
const ParamsRouteRoute = ParamsRouteRouteImport.update({
@@ -43,21 +38,26 @@ const ParamsRouteRoute = ParamsRouteRouteImport.update({
path: '/params',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const RelativeRoute = RelativeRouteImport.update({
+ id: '/relative',
+ path: '/relative',
getParentRoute: () => rootRouteImport,
} as any)
-const SearchSearchPlaceholderRoute = SearchSearchPlaceholderRouteImport.update({
- id: '/searchPlaceholder',
- path: '/searchPlaceholder',
- getParentRoute: () => SearchRouteRoute,
+const SearchRouteRoute = SearchRouteRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => rootRouteImport,
} as any)
const ParamsParamsPlaceholderRoute = ParamsParamsPlaceholderRouteImport.update({
id: '/$paramsPlaceholder',
path: '/$paramsPlaceholder',
getParentRoute: () => ParamsRouteRoute,
} as any)
+const SearchSearchPlaceholderRoute = SearchSearchPlaceholderRouteImport.update({
+ id: '/searchPlaceholder',
+ path: '/searchPlaceholder',
+ getParentRoute: () => SearchRouteRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -134,18 +134,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/relative': {
- id: '/relative'
- path: '/relative'
- fullPath: '/relative'
- preLoaderRoute: typeof RelativeRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/linkProps': {
- id: '/linkProps'
- path: '/linkProps'
- fullPath: '/linkProps'
- preLoaderRoute: typeof LinkPropsRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/absolute': {
@@ -155,11 +148,11 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AbsoluteRouteImport
parentRoute: typeof rootRouteImport
}
- '/search': {
- id: '/search'
- path: '/search'
- fullPath: '/search'
- preLoaderRoute: typeof SearchRouteRouteImport
+ '/linkProps': {
+ id: '/linkProps'
+ path: '/linkProps'
+ fullPath: '/linkProps'
+ preLoaderRoute: typeof LinkPropsRouteImport
parentRoute: typeof rootRouteImport
}
'/params': {
@@ -169,19 +162,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ParamsRouteRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/relative': {
+ id: '/relative'
+ path: '/relative'
+ fullPath: '/relative'
+ preLoaderRoute: typeof RelativeRouteImport
parentRoute: typeof rootRouteImport
}
- '/search/searchPlaceholder': {
- id: '/search/searchPlaceholder'
- path: '/searchPlaceholder'
- fullPath: '/search/searchPlaceholder'
- preLoaderRoute: typeof SearchSearchPlaceholderRouteImport
- parentRoute: typeof SearchRouteRoute
+ '/search': {
+ id: '/search'
+ path: '/search'
+ fullPath: '/search'
+ preLoaderRoute: typeof SearchRouteRouteImport
+ parentRoute: typeof rootRouteImport
}
'/params/$paramsPlaceholder': {
id: '/params/$paramsPlaceholder'
@@ -190,6 +183,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ParamsParamsPlaceholderRouteImport
parentRoute: typeof ParamsRouteRoute
}
+ '/search/searchPlaceholder': {
+ id: '/search/searchPlaceholder'
+ path: '/searchPlaceholder'
+ fullPath: '/search/searchPlaceholder'
+ preLoaderRoute: typeof SearchSearchPlaceholderRouteImport
+ parentRoute: typeof SearchRouteRoute
+ }
}
}
diff --git a/examples/solid/location-masking/package.json b/examples/solid/location-masking/package.json
index 41f84f5eb0..798c32571d 100644
--- a/examples/solid/location-masking/package.json
+++ b/examples/solid/location-masking/package.json
@@ -19,7 +19,8 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/navigation-blocking/package.json b/examples/solid/navigation-blocking/package.json
index 42e742be94..7c9b08a63d 100644
--- a/examples/solid/navigation-blocking/package.json
+++ b/examples/solid/navigation-blocking/package.json
@@ -19,7 +19,8 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/quickstart-esbuild-file-based/package.json b/examples/solid/quickstart-esbuild-file-based/package.json
index ae4ebdfce3..5c17de060e 100644
--- a/examples/solid/quickstart-esbuild-file-based/package.json
+++ b/examples/solid/quickstart-esbuild-file-based/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"redaxios": "^0.5.1",
diff --git a/examples/solid/quickstart-file-based/package.json b/examples/solid/quickstart-file-based/package.json
index 42a109ea12..56286aa8fc 100644
--- a/examples/solid/quickstart-file-based/package.json
+++ b/examples/solid/quickstart-file-based/package.json
@@ -19,8 +19,9 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "@tanstack/router-plugin": "^1.168.19",
- "typescript": "^6.0.2",
+ "@tanstack/router-plugin": "^1.168.24",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/quickstart-file-based/src/routeTree.gen.ts b/examples/solid/quickstart-file-based/src/routeTree.gen.ts
index 333a815c38..98df1e83d8 100644
--- a/examples/solid/quickstart-file-based/src/routeTree.gen.ts
+++ b/examples/solid/quickstart-file-based/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AboutRouteImport } from './routes/about'
-const AboutRoute = AboutRouteImport.update({
- id: '/about',
- path: '/about',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/about': {
- id: '/about'
- path: '/about'
- fullPath: '/about'
- preLoaderRoute: typeof AboutRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/quickstart-rspack-file-based/package.json b/examples/solid/quickstart-rspack-file-based/package.json
index 8077f0114f..4d3e6afa50 100644
--- a/examples/solid/quickstart-rspack-file-based/package.json
+++ b/examples/solid/quickstart-rspack-file-based/package.json
@@ -17,10 +17,11 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
- "@rsbuild/core": "^2.0.11",
+ "@rsbuild/core": "^2.1.0",
"@rsbuild/plugin-babel": "^1.1.2",
"@rsbuild/plugin-solid": "^2.0.0-beta.0",
- "@tanstack/router-plugin": "^1.168.19",
- "typescript": "^6.0.2"
+ "@tanstack/router-plugin": "^1.168.24",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2"
}
}
diff --git a/examples/solid/quickstart-rspack-file-based/src/routeTree.gen.ts b/examples/solid/quickstart-rspack-file-based/src/routeTree.gen.ts
index 333a815c38..98df1e83d8 100644
--- a/examples/solid/quickstart-rspack-file-based/src/routeTree.gen.ts
+++ b/examples/solid/quickstart-rspack-file-based/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AboutRouteImport } from './routes/about'
-const AboutRoute = AboutRouteImport.update({
- id: '/about',
- path: '/about',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/about': {
- id: '/about'
- path: '/about'
- fullPath: '/about'
- preLoaderRoute: typeof AboutRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/quickstart-webpack-file-based/package.json b/examples/solid/quickstart-webpack-file-based/package.json
index cacc1b810c..cc7ceb4789 100644
--- a/examples/solid/quickstart-webpack-file-based/package.json
+++ b/examples/solid/quickstart-webpack-file-based/package.json
@@ -16,7 +16,7 @@
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-typescript": "^7.27.1",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"babel-loader": "^10.0.0",
"babel-preset-solid": "2.0.0-beta.29",
"css-loader": "^7.1.2",
@@ -24,7 +24,8 @@
"postcss": "^8.5.6",
"postcss-loader": "^8.2.0",
"style-loader": "^4.0.0",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"webpack": "^5.97.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.2.4"
diff --git a/examples/solid/quickstart-webpack-file-based/src/routeTree.gen.ts b/examples/solid/quickstart-webpack-file-based/src/routeTree.gen.ts
index 333a815c38..98df1e83d8 100644
--- a/examples/solid/quickstart-webpack-file-based/src/routeTree.gen.ts
+++ b/examples/solid/quickstart-webpack-file-based/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AboutRouteImport } from './routes/about'
-const AboutRoute = AboutRouteImport.update({
- id: '/about',
- path: '/about',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/about': {
- id: '/about'
- path: '/about'
- fullPath: '/about'
- preLoaderRoute: typeof AboutRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/quickstart/package.json b/examples/solid/quickstart/package.json
index 3c1dd46d2b..63632b7acc 100644
--- a/examples/solid/quickstart/package.json
+++ b/examples/solid/quickstart/package.json
@@ -17,7 +17,8 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/router-monorepo-simple-lazy/package.json b/examples/solid/router-monorepo-simple-lazy/package.json
index d801c9179b..6f6377a782 100644
--- a/examples/solid/router-monorepo-simple-lazy/package.json
+++ b/examples/solid/router-monorepo-simple-lazy/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"redaxios": "^0.5.1",
@@ -17,7 +17,8 @@
},
"devDependencies": {
"@types/node": "^22.7.4",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-solid": "^3.0.0-next.21"
diff --git a/examples/solid/router-monorepo-simple-lazy/packages/app/package.json b/examples/solid/router-monorepo-simple-lazy/packages/app/package.json
index 78bfaab83a..cc8ae21100 100644
--- a/examples/solid/router-monorepo-simple-lazy/packages/app/package.json
+++ b/examples/solid/router-monorepo-simple-lazy/packages/app/package.json
@@ -18,7 +18,8 @@
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"tailwindcss": "^4.2.2",
"vite": "^8.0.14",
diff --git a/examples/solid/router-monorepo-simple-lazy/packages/post-feature/package.json b/examples/solid/router-monorepo-simple-lazy/packages/post-feature/package.json
index 810785366b..18d423ba68 100644
--- a/examples/solid/router-monorepo-simple-lazy/packages/post-feature/package.json
+++ b/examples/solid/router-monorepo-simple-lazy/packages/post-feature/package.json
@@ -23,7 +23,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4"
}
diff --git a/examples/solid/router-monorepo-simple-lazy/packages/router/package.json b/examples/solid/router-monorepo-simple-lazy/packages/router/package.json
index acfeccdfe7..27a235f483 100644
--- a/examples/solid/router-monorepo-simple-lazy/packages/router/package.json
+++ b/examples/solid/router-monorepo-simple-lazy/packages/router/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"@tanstack/history": "^1.162.0",
"@tanstack/solid-router": "^2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"redaxios": "^0.5.1",
"zod": "^4.4.3",
"solid-js": "2.0.0-beta.29",
@@ -18,7 +18,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4"
}
diff --git a/examples/solid/router-monorepo-simple-lazy/packages/router/src/routeTree.gen.ts b/examples/solid/router-monorepo-simple-lazy/packages/router/src/routeTree.gen.ts
index e8f1a5370b..8760312d24 100644
--- a/examples/solid/router-monorepo-simple-lazy/packages/router/src/routeTree.gen.ts
+++ b/examples/solid/router-monorepo-simple-lazy/packages/router/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as PostIdRouteImport } from './routes/$postId'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as PostIdRouteImport } from './routes/$postId'
-const PostIdRoute = PostIdRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const PostIdRoute = PostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/$postId': {
- id: '/$postId'
- path: '/$postId'
- fullPath: '/$postId'
- preLoaderRoute: typeof PostIdRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/$postId': {
+ id: '/$postId'
+ path: '/$postId'
+ fullPath: '/$postId'
+ preLoaderRoute: typeof PostIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/router-monorepo-simple/package.json b/examples/solid/router-monorepo-simple/package.json
index ff00c5c03b..1cd40de1c2 100644
--- a/examples/solid/router-monorepo-simple/package.json
+++ b/examples/solid/router-monorepo-simple/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"redaxios": "^0.5.1",
@@ -17,7 +17,8 @@
},
"devDependencies": {
"@types/node": "^22.7.4",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-solid": "^3.0.0-next.21"
diff --git a/examples/solid/router-monorepo-simple/packages/app/package.json b/examples/solid/router-monorepo-simple/packages/app/package.json
index a4f11e5f65..439bf2fbc5 100644
--- a/examples/solid/router-monorepo-simple/packages/app/package.json
+++ b/examples/solid/router-monorepo-simple/packages/app/package.json
@@ -16,7 +16,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"vite": "^8.0.14",
"tailwindcss": "^4.2.2",
diff --git a/examples/solid/router-monorepo-simple/packages/post-feature/package.json b/examples/solid/router-monorepo-simple/packages/post-feature/package.json
index 4f9acc251c..77683a1b81 100644
--- a/examples/solid/router-monorepo-simple/packages/post-feature/package.json
+++ b/examples/solid/router-monorepo-simple/packages/post-feature/package.json
@@ -14,7 +14,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4"
}
diff --git a/examples/solid/router-monorepo-simple/packages/router/package.json b/examples/solid/router-monorepo-simple/packages/router/package.json
index 9b5353fd75..46a3da3a27 100644
--- a/examples/solid/router-monorepo-simple/packages/router/package.json
+++ b/examples/solid/router-monorepo-simple/packages/router/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"@tanstack/history": "^1.162.0",
"@tanstack/solid-router": "^2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"redaxios": "^0.5.1",
"zod": "^4.4.3",
"solid-js": "2.0.0-beta.29",
@@ -18,7 +18,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4"
}
diff --git a/examples/solid/router-monorepo-simple/packages/router/src/routeTree.gen.ts b/examples/solid/router-monorepo-simple/packages/router/src/routeTree.gen.ts
index e8f1a5370b..8760312d24 100644
--- a/examples/solid/router-monorepo-simple/packages/router/src/routeTree.gen.ts
+++ b/examples/solid/router-monorepo-simple/packages/router/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as PostIdRouteImport } from './routes/$postId'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as PostIdRouteImport } from './routes/$postId'
-const PostIdRoute = PostIdRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const PostIdRoute = PostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/$postId': {
- id: '/$postId'
- path: '/$postId'
- fullPath: '/$postId'
- preLoaderRoute: typeof PostIdRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/$postId': {
+ id: '/$postId'
+ path: '/$postId'
+ fullPath: '/$postId'
+ preLoaderRoute: typeof PostIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/router-monorepo-solid-query/package.json b/examples/solid/router-monorepo-solid-query/package.json
index d131143aac..9847559289 100644
--- a/examples/solid/router-monorepo-solid-query/package.json
+++ b/examples/solid/router-monorepo-solid-query/package.json
@@ -11,7 +11,7 @@
},
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-query": "^6.0.0-beta.7",
"@tanstack/solid-query-devtools": "^6.0.0-beta.7",
"@tanstack/solid-router": "^2.0.0-beta.29",
@@ -21,7 +21,8 @@
},
"devDependencies": {
"@types/node": "^22.10.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-solid": "^3.0.0-next.21"
diff --git a/examples/solid/router-monorepo-solid-query/packages/app/package.json b/examples/solid/router-monorepo-solid-query/packages/app/package.json
index 66b0ee1c47..a88ad0b0bb 100644
--- a/examples/solid/router-monorepo-solid-query/packages/app/package.json
+++ b/examples/solid/router-monorepo-solid-query/packages/app/package.json
@@ -17,7 +17,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"tailwindcss": "^4.2.2",
"vite": "^8.0.14",
diff --git a/examples/solid/router-monorepo-solid-query/packages/post-feature/package.json b/examples/solid/router-monorepo-solid-query/packages/post-feature/package.json
index d301062492..cf358fe85a 100644
--- a/examples/solid/router-monorepo-solid-query/packages/post-feature/package.json
+++ b/examples/solid/router-monorepo-solid-query/packages/post-feature/package.json
@@ -16,7 +16,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4"
}
diff --git a/examples/solid/router-monorepo-solid-query/packages/post-query/package.json b/examples/solid/router-monorepo-solid-query/packages/post-query/package.json
index 188b57ee21..284b46f351 100644
--- a/examples/solid/router-monorepo-solid-query/packages/post-query/package.json
+++ b/examples/solid/router-monorepo-solid-query/packages/post-query/package.json
@@ -14,7 +14,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14"
}
}
diff --git a/examples/solid/router-monorepo-solid-query/packages/router/package.json b/examples/solid/router-monorepo-solid-query/packages/router/package.json
index ee8ffa6386..0c43684fd6 100644
--- a/examples/solid/router-monorepo-solid-query/packages/router/package.json
+++ b/examples/solid/router-monorepo-solid-query/packages/router/package.json
@@ -11,7 +11,7 @@
"@tanstack/history": "^1.162.0",
"@tanstack/solid-query": "^6.0.0-beta.7",
"@tanstack/solid-router": "^2.0.0-beta.29",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@router-solid-mono-solid-query/post-query": "workspace:*",
"redaxios": "^0.5.1",
"zod": "^4.4.3",
@@ -20,7 +20,8 @@
},
"devDependencies": {
"vite-plugin-solid": "^3.0.0-next.21",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-dts": "^4.5.4"
}
diff --git a/examples/solid/router-monorepo-solid-query/packages/router/src/routeTree.gen.ts b/examples/solid/router-monorepo-solid-query/packages/router/src/routeTree.gen.ts
index e8f1a5370b..8760312d24 100644
--- a/examples/solid/router-monorepo-solid-query/packages/router/src/routeTree.gen.ts
+++ b/examples/solid/router-monorepo-solid-query/packages/router/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as PostIdRouteImport } from './routes/$postId'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as PostIdRouteImport } from './routes/$postId'
-const PostIdRoute = PostIdRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const PostIdRoute = PostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/$postId': {
- id: '/$postId'
- path: '/$postId'
- fullPath: '/$postId'
- preLoaderRoute: typeof PostIdRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/$postId': {
+ id: '/$postId'
+ path: '/$postId'
+ fullPath: '/$postId'
+ preLoaderRoute: typeof PostIdRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/scroll-restoration/package.json b/examples/solid/scroll-restoration/package.json
index 6f62372dc5..96628d01df 100644
--- a/examples/solid/scroll-restoration/package.json
+++ b/examples/solid/scroll-restoration/package.json
@@ -18,7 +18,8 @@
"tailwindcss": "^4.2.2"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/search-validator-adapters/package.json b/examples/solid/search-validator-adapters/package.json
index 79a7c2a655..779a55a460 100644
--- a/examples/solid/search-validator-adapters/package.json
+++ b/examples/solid/search-validator-adapters/package.json
@@ -13,7 +13,7 @@
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/arktype-adapter": "^1.167.0",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-query": "^6.0.0-beta.7",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
@@ -28,7 +28,8 @@
"devDependencies": {
"@solidjs/testing-library": "^0.8.10",
"@testing-library/jest-dom": "^6.6.3",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/search-validator-adapters/src/routeTree.gen.ts b/examples/solid/search-validator-adapters/src/routeTree.gen.ts
index 24089ece84..4cb97b08e9 100644
--- a/examples/solid/search-validator-adapters/src/routeTree.gen.ts
+++ b/examples/solid/search-validator-adapters/src/routeTree.gen.ts
@@ -10,18 +10,18 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as UsersZodIndexRouteImport } from './routes/users/zod.index'
-import { Route as UsersValibotIndexRouteImport } from './routes/users/valibot.index'
import { Route as UsersArktypeIndexRouteImport } from './routes/users/arktype.index'
+import { Route as UsersValibotIndexRouteImport } from './routes/users/valibot.index'
+import { Route as UsersZodIndexRouteImport } from './routes/users/zod.index'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const UsersZodIndexRoute = UsersZodIndexRouteImport.update({
- id: '/users/zod/',
- path: '/users/zod/',
+const UsersArktypeIndexRoute = UsersArktypeIndexRouteImport.update({
+ id: '/users/arktype/',
+ path: '/users/arktype/',
getParentRoute: () => rootRouteImport,
} as any)
const UsersValibotIndexRoute = UsersValibotIndexRouteImport.update({
@@ -29,9 +29,9 @@ const UsersValibotIndexRoute = UsersValibotIndexRouteImport.update({
path: '/users/valibot/',
getParentRoute: () => rootRouteImport,
} as any)
-const UsersArktypeIndexRoute = UsersArktypeIndexRouteImport.update({
- id: '/users/arktype/',
- path: '/users/arktype/',
+const UsersZodIndexRoute = UsersZodIndexRouteImport.update({
+ id: '/users/zod/',
+ path: '/users/zod/',
getParentRoute: () => rootRouteImport,
} as any)
@@ -78,11 +78,11 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/users/zod/': {
- id: '/users/zod/'
- path: '/users/zod'
- fullPath: '/users/zod/'
- preLoaderRoute: typeof UsersZodIndexRouteImport
+ '/users/arktype/': {
+ id: '/users/arktype/'
+ path: '/users/arktype'
+ fullPath: '/users/arktype/'
+ preLoaderRoute: typeof UsersArktypeIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/users/valibot/': {
@@ -92,11 +92,11 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof UsersValibotIndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/users/arktype/': {
- id: '/users/arktype/'
- path: '/users/arktype'
- fullPath: '/users/arktype/'
- preLoaderRoute: typeof UsersArktypeIndexRouteImport
+ '/users/zod/': {
+ id: '/users/zod/'
+ path: '/users/zod'
+ fullPath: '/users/zod/'
+ preLoaderRoute: typeof UsersZodIndexRouteImport
parentRoute: typeof rootRouteImport
}
}
diff --git a/examples/solid/start-basic-auth/package.json b/examples/solid/start-basic-auth/package.json
index e7901ea5a3..5d6db74dec 100644
--- a/examples/solid/start-basic-auth/package.json
+++ b/examples/solid/start-basic-auth/package.json
@@ -28,7 +28,8 @@
"dotenv": "^17.2.3",
"prisma": "^7.0.0",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-basic-auth/src/routeTree.gen.ts b/examples/solid/start-basic-auth/src/routeTree.gen.ts
index bad0f1b074..07924d102a 100644
--- a/examples/solid/start-basic-auth/src/routeTree.gen.ts
+++ b/examples/solid/start-basic-auth/src/routeTree.gen.ts
@@ -9,23 +9,22 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as SignupRouteImport } from './routes/signup'
-import { Route as LogoutRouteImport } from './routes/logout'
-import { Route as LoginRouteImport } from './routes/login'
-import { Route as AuthedRouteImport } from './routes/_authed'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AuthedRouteImport } from './routes/_authed'
+import { Route as LoginRouteImport } from './routes/login'
+import { Route as LogoutRouteImport } from './routes/logout'
+import { Route as SignupRouteImport } from './routes/signup'
import { Route as AuthedPostsRouteRouteImport } from './routes/_authed/posts.route'
import { Route as AuthedPostsIndexRouteImport } from './routes/_authed/posts.index'
import { Route as AuthedPostsPostIdRouteImport } from './routes/_authed/posts.$postId'
-const SignupRoute = SignupRouteImport.update({
- id: '/signup',
- path: '/signup',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const LogoutRoute = LogoutRouteImport.update({
- id: '/logout',
- path: '/logout',
+const AuthedRoute = AuthedRouteImport.update({
+ id: '/_authed',
getParentRoute: () => rootRouteImport,
} as any)
const LoginRoute = LoginRouteImport.update({
@@ -33,13 +32,14 @@ const LoginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
-const AuthedRoute = AuthedRouteImport.update({
- id: '/_authed',
+const LogoutRoute = LogoutRouteImport.update({
+ id: '/logout',
+ path: '/logout',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const SignupRoute = SignupRouteImport.update({
+ id: '/signup',
+ path: '/signup',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedPostsRouteRoute = AuthedPostsRouteRouteImport.update({
@@ -120,18 +120,18 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/signup': {
- id: '/signup'
- path: '/signup'
- fullPath: '/signup'
- preLoaderRoute: typeof SignupRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/logout': {
- id: '/logout'
- path: '/logout'
- fullPath: '/logout'
- preLoaderRoute: typeof LogoutRouteImport
+ '/_authed': {
+ id: '/_authed'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof AuthedRouteImport
parentRoute: typeof rootRouteImport
}
'/login': {
@@ -141,18 +141,18 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
- '/_authed': {
- id: '/_authed'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof AuthedRouteImport
+ '/logout': {
+ id: '/logout'
+ path: '/logout'
+ fullPath: '/logout'
+ preLoaderRoute: typeof LogoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/signup': {
+ id: '/signup'
+ path: '/signup'
+ fullPath: '/signup'
+ preLoaderRoute: typeof SignupRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/posts': {
diff --git a/examples/solid/start-basic-authjs/package.json b/examples/solid/start-basic-authjs/package.json
index e631440e13..ffdda5c1a4 100644
--- a/examples/solid/start-basic-authjs/package.json
+++ b/examples/solid/start-basic-authjs/package.json
@@ -23,7 +23,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-basic-authjs/src/routeTree.gen.ts b/examples/solid/start-basic-authjs/src/routeTree.gen.ts
index e772ba9c3e..7cc6d28cf7 100644
--- a/examples/solid/start-basic-authjs/src/routeTree.gen.ts
+++ b/examples/solid/start-basic-authjs/src/routeTree.gen.ts
@@ -9,14 +9,14 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as ProtectedRouteImport } from './routes/protected'
-import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as LoginRouteImport } from './routes/login'
+import { Route as ProtectedRouteImport } from './routes/protected'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
-const ProtectedRoute = ProtectedRouteImport.update({
- id: '/protected',
- path: '/protected',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const LoginRoute = LoginRouteImport.update({
@@ -24,9 +24,9 @@ const LoginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const ProtectedRoute = ProtectedRouteImport.update({
+ id: '/protected',
+ path: '/protected',
getParentRoute: () => rootRouteImport,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
@@ -71,11 +71,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/protected': {
- id: '/protected'
- path: '/protected'
- fullPath: '/protected'
- preLoaderRoute: typeof ProtectedRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/login': {
@@ -85,11 +85,11 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/protected': {
+ id: '/protected'
+ path: '/protected'
+ fullPath: '/protected'
+ preLoaderRoute: typeof ProtectedRouteImport
parentRoute: typeof rootRouteImport
}
'/api/auth/$': {
diff --git a/examples/solid/start-basic-cloudflare/package.json b/examples/solid/start-basic-cloudflare/package.json
index 868fa166db..e0cf2c2b5c 100644
--- a/examples/solid/start-basic-cloudflare/package.json
+++ b/examples/solid/start-basic-cloudflare/package.json
@@ -23,7 +23,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4",
diff --git a/examples/solid/start-basic-cloudflare/src/routeTree.gen.ts b/examples/solid/start-basic-cloudflare/src/routeTree.gen.ts
index c3f6976d03..7cba7e8958 100644
--- a/examples/solid/start-basic-cloudflare/src/routeTree.gen.ts
+++ b/examples/solid/start-basic-cloudflare/src/routeTree.gen.ts
@@ -9,37 +9,36 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as UsersRouteImport } from './routes/users'
-import { Route as RedirectRouteImport } from './routes/redirect'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as DeferredRouteImport } from './routes/deferred'
-import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RedirectRouteImport } from './routes/redirect'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
+import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
-import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as ApiUsersRouteImport } from './routes/api/users'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-const UsersRoute = UsersRouteImport.update({
- id: '/users',
- path: '/users',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const RedirectRoute = RedirectRouteImport.update({
- id: '/redirect',
- path: '/redirect',
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
} as any)
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const CustomScriptDotjsRoute = CustomScriptDotjsRouteImport.update({
+ id: '/customScript.js',
+ path: '/customScript.js',
getParentRoute: () => rootRouteImport,
} as any)
const DeferredRoute = DeferredRouteImport.update({
@@ -47,72 +46,73 @@ const DeferredRoute = DeferredRouteImport.update({
path: '/deferred',
getParentRoute: () => rootRouteImport,
} as any)
-const CustomScriptDotjsRoute = CustomScriptDotjsRouteImport.update({
- id: '/customScript.js',
- path: '/customScript.js',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const RedirectRoute = RedirectRouteImport.update({
+ id: '/redirect',
+ path: '/redirect',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const UsersRoute = UsersRouteImport.update({
+ id: '/users',
+ path: '/users',
getParentRoute: () => rootRouteImport,
} as any)
-const UsersIndexRoute = UsersIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => UsersRoute,
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
+ getParentRoute: () => rootRouteImport,
} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PostsRoute,
} as any)
-const UsersUserIdRoute = UsersUserIdRouteImport.update({
- id: '/$userId',
- path: '/$userId',
- getParentRoute: () => UsersRoute,
-} as any)
const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const ApiUsersRoute = ApiUsersRouteImport.update({
- id: '/api/users',
- path: '/api/users',
- getParentRoute: () => rootRouteImport,
-} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
- } as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
- id: '/posts_/$postId/deep',
- path: '/posts/$postId/deep',
- getParentRoute: () => rootRouteImport,
+const UsersIndexRoute = UsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => UsersRoute,
} as any)
-const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
- getParentRoute: () => ApiUsersRoute,
+ getParentRoute: () => UsersRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteBRoute =
- PathlessLayoutNestedLayoutRouteBRouteImport.update({
- id: '/route-b',
- path: '/route-b',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
const PathlessLayoutNestedLayoutRouteARoute =
PathlessLayoutNestedLayoutRouteARouteImport.update({
id: '/route-a',
path: '/route-a',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
+const PathlessLayoutNestedLayoutRouteBRoute =
+ PathlessLayoutNestedLayoutRouteBRouteImport.update({
+ id: '/route-b',
+ path: '/route-b',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
+ } as any)
+const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -234,25 +234,25 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/users': {
- id: '/users'
- path: '/users'
- fullPath: '/users'
- preLoaderRoute: typeof UsersRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/redirect': {
- id: '/redirect'
- path: '/redirect'
- fullPath: '/redirect'
- preLoaderRoute: typeof RedirectRouteImport
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/customScript.js': {
+ id: '/customScript.js'
+ path: '/customScript.js'
+ fullPath: '/customScript.js'
+ preLoaderRoute: typeof CustomScriptDotjsRouteImport
parentRoute: typeof rootRouteImport
}
'/deferred': {
@@ -262,33 +262,40 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
- '/customScript.js': {
- id: '/customScript.js'
- path: '/customScript.js'
- fullPath: '/customScript.js'
- preLoaderRoute: typeof CustomScriptDotjsRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ '/redirect': {
+ id: '/redirect'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRouteImport
}
- '/users/': {
- id: '/users/'
- path: '/'
- fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexRouteImport
- parentRoute: typeof UsersRoute
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
+ }
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRouteImport
}
'/posts/': {
id: '/posts/'
@@ -297,13 +304,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsIndexRouteImport
parentRoute: typeof PostsRoute
}
- '/users/$userId': {
- id: '/users/$userId'
- path: '/$userId'
- fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdRouteImport
- parentRoute: typeof UsersRoute
- }
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
@@ -311,33 +311,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/api/users': {
- id: '/api/users'
- path: '/api/users'
- fullPath: '/api/users'
- preLoaderRoute: typeof ApiUsersRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
- }
- '/posts_/$postId/deep': {
- id: '/posts_/$postId/deep'
- path: '/posts/$postId/deep'
- fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepRouteImport
- parentRoute: typeof rootRouteImport
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRoute
}
- '/api/users/$userId': {
- id: '/api/users/$userId'
+ '/users/$userId': {
+ id: '/users/$userId'
path: '/$userId'
- fullPath: '/api/users/$userId'
- preLoaderRoute: typeof ApiUsersUserIdRouteImport
- parentRoute: typeof ApiUsersRoute
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -346,12 +339,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
+ '/api/users/$userId': {
+ id: '/api/users/$userId'
+ path: '/$userId'
+ fullPath: '/api/users/$userId'
+ preLoaderRoute: typeof ApiUsersUserIdRouteImport
+ parentRoute: typeof ApiUsersRoute
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
}
}
}
diff --git a/examples/solid/start-basic-netlify/package.json b/examples/solid/start-basic-netlify/package.json
index af3019ed97..629f752b30 100644
--- a/examples/solid/start-basic-netlify/package.json
+++ b/examples/solid/start-basic-netlify/package.json
@@ -20,7 +20,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-basic-netlify/src/routeTree.gen.ts b/examples/solid/start-basic-netlify/src/routeTree.gen.ts
index c3f6976d03..7cba7e8958 100644
--- a/examples/solid/start-basic-netlify/src/routeTree.gen.ts
+++ b/examples/solid/start-basic-netlify/src/routeTree.gen.ts
@@ -9,37 +9,36 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as UsersRouteImport } from './routes/users'
-import { Route as RedirectRouteImport } from './routes/redirect'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as DeferredRouteImport } from './routes/deferred'
-import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RedirectRouteImport } from './routes/redirect'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
+import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
-import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as ApiUsersRouteImport } from './routes/api/users'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-const UsersRoute = UsersRouteImport.update({
- id: '/users',
- path: '/users',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const RedirectRoute = RedirectRouteImport.update({
- id: '/redirect',
- path: '/redirect',
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
} as any)
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const CustomScriptDotjsRoute = CustomScriptDotjsRouteImport.update({
+ id: '/customScript.js',
+ path: '/customScript.js',
getParentRoute: () => rootRouteImport,
} as any)
const DeferredRoute = DeferredRouteImport.update({
@@ -47,72 +46,73 @@ const DeferredRoute = DeferredRouteImport.update({
path: '/deferred',
getParentRoute: () => rootRouteImport,
} as any)
-const CustomScriptDotjsRoute = CustomScriptDotjsRouteImport.update({
- id: '/customScript.js',
- path: '/customScript.js',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const RedirectRoute = RedirectRouteImport.update({
+ id: '/redirect',
+ path: '/redirect',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const UsersRoute = UsersRouteImport.update({
+ id: '/users',
+ path: '/users',
getParentRoute: () => rootRouteImport,
} as any)
-const UsersIndexRoute = UsersIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => UsersRoute,
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
+ getParentRoute: () => rootRouteImport,
} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PostsRoute,
} as any)
-const UsersUserIdRoute = UsersUserIdRouteImport.update({
- id: '/$userId',
- path: '/$userId',
- getParentRoute: () => UsersRoute,
-} as any)
const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const ApiUsersRoute = ApiUsersRouteImport.update({
- id: '/api/users',
- path: '/api/users',
- getParentRoute: () => rootRouteImport,
-} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
- } as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
- id: '/posts_/$postId/deep',
- path: '/posts/$postId/deep',
- getParentRoute: () => rootRouteImport,
+const UsersIndexRoute = UsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => UsersRoute,
} as any)
-const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
- getParentRoute: () => ApiUsersRoute,
+ getParentRoute: () => UsersRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteBRoute =
- PathlessLayoutNestedLayoutRouteBRouteImport.update({
- id: '/route-b',
- path: '/route-b',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
const PathlessLayoutNestedLayoutRouteARoute =
PathlessLayoutNestedLayoutRouteARouteImport.update({
id: '/route-a',
path: '/route-a',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
+const PathlessLayoutNestedLayoutRouteBRoute =
+ PathlessLayoutNestedLayoutRouteBRouteImport.update({
+ id: '/route-b',
+ path: '/route-b',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
+ } as any)
+const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -234,25 +234,25 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/users': {
- id: '/users'
- path: '/users'
- fullPath: '/users'
- preLoaderRoute: typeof UsersRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/redirect': {
- id: '/redirect'
- path: '/redirect'
- fullPath: '/redirect'
- preLoaderRoute: typeof RedirectRouteImport
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/customScript.js': {
+ id: '/customScript.js'
+ path: '/customScript.js'
+ fullPath: '/customScript.js'
+ preLoaderRoute: typeof CustomScriptDotjsRouteImport
parentRoute: typeof rootRouteImport
}
'/deferred': {
@@ -262,33 +262,40 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
- '/customScript.js': {
- id: '/customScript.js'
- path: '/customScript.js'
- fullPath: '/customScript.js'
- preLoaderRoute: typeof CustomScriptDotjsRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ '/redirect': {
+ id: '/redirect'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRouteImport
}
- '/users/': {
- id: '/users/'
- path: '/'
- fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexRouteImport
- parentRoute: typeof UsersRoute
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
+ }
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRouteImport
}
'/posts/': {
id: '/posts/'
@@ -297,13 +304,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsIndexRouteImport
parentRoute: typeof PostsRoute
}
- '/users/$userId': {
- id: '/users/$userId'
- path: '/$userId'
- fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdRouteImport
- parentRoute: typeof UsersRoute
- }
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
@@ -311,33 +311,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/api/users': {
- id: '/api/users'
- path: '/api/users'
- fullPath: '/api/users'
- preLoaderRoute: typeof ApiUsersRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
- }
- '/posts_/$postId/deep': {
- id: '/posts_/$postId/deep'
- path: '/posts/$postId/deep'
- fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepRouteImport
- parentRoute: typeof rootRouteImport
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRoute
}
- '/api/users/$userId': {
- id: '/api/users/$userId'
+ '/users/$userId': {
+ id: '/users/$userId'
path: '/$userId'
- fullPath: '/api/users/$userId'
- preLoaderRoute: typeof ApiUsersUserIdRouteImport
- parentRoute: typeof ApiUsersRoute
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -346,12 +339,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
+ '/api/users/$userId': {
+ id: '/api/users/$userId'
+ path: '/$userId'
+ fullPath: '/api/users/$userId'
+ preLoaderRoute: typeof ApiUsersUserIdRouteImport
+ parentRoute: typeof ApiUsersRoute
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
}
}
}
diff --git a/examples/solid/start-basic-nitro/package.json b/examples/solid/start-basic-nitro/package.json
index a2c223c030..9a32a79bf0 100644
--- a/examples/solid/start-basic-nitro/package.json
+++ b/examples/solid/start-basic-nitro/package.json
@@ -20,7 +20,8 @@
"@types/node": "^22.5.4",
"nitro": "^3.0.260311-beta",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-basic-nitro/src/routeTree.gen.ts b/examples/solid/start-basic-nitro/src/routeTree.gen.ts
index c3f6976d03..7cba7e8958 100644
--- a/examples/solid/start-basic-nitro/src/routeTree.gen.ts
+++ b/examples/solid/start-basic-nitro/src/routeTree.gen.ts
@@ -9,37 +9,36 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as UsersRouteImport } from './routes/users'
-import { Route as RedirectRouteImport } from './routes/redirect'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as DeferredRouteImport } from './routes/deferred'
-import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RedirectRouteImport } from './routes/redirect'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
+import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
-import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as ApiUsersRouteImport } from './routes/api/users'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-const UsersRoute = UsersRouteImport.update({
- id: '/users',
- path: '/users',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const RedirectRoute = RedirectRouteImport.update({
- id: '/redirect',
- path: '/redirect',
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
} as any)
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const CustomScriptDotjsRoute = CustomScriptDotjsRouteImport.update({
+ id: '/customScript.js',
+ path: '/customScript.js',
getParentRoute: () => rootRouteImport,
} as any)
const DeferredRoute = DeferredRouteImport.update({
@@ -47,72 +46,73 @@ const DeferredRoute = DeferredRouteImport.update({
path: '/deferred',
getParentRoute: () => rootRouteImport,
} as any)
-const CustomScriptDotjsRoute = CustomScriptDotjsRouteImport.update({
- id: '/customScript.js',
- path: '/customScript.js',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const RedirectRoute = RedirectRouteImport.update({
+ id: '/redirect',
+ path: '/redirect',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const UsersRoute = UsersRouteImport.update({
+ id: '/users',
+ path: '/users',
getParentRoute: () => rootRouteImport,
} as any)
-const UsersIndexRoute = UsersIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => UsersRoute,
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
+ getParentRoute: () => rootRouteImport,
} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PostsRoute,
} as any)
-const UsersUserIdRoute = UsersUserIdRouteImport.update({
- id: '/$userId',
- path: '/$userId',
- getParentRoute: () => UsersRoute,
-} as any)
const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const ApiUsersRoute = ApiUsersRouteImport.update({
- id: '/api/users',
- path: '/api/users',
- getParentRoute: () => rootRouteImport,
-} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
- } as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
- id: '/posts_/$postId/deep',
- path: '/posts/$postId/deep',
- getParentRoute: () => rootRouteImport,
+const UsersIndexRoute = UsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => UsersRoute,
} as any)
-const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
- getParentRoute: () => ApiUsersRoute,
+ getParentRoute: () => UsersRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteBRoute =
- PathlessLayoutNestedLayoutRouteBRouteImport.update({
- id: '/route-b',
- path: '/route-b',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
const PathlessLayoutNestedLayoutRouteARoute =
PathlessLayoutNestedLayoutRouteARouteImport.update({
id: '/route-a',
path: '/route-a',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
+const PathlessLayoutNestedLayoutRouteBRoute =
+ PathlessLayoutNestedLayoutRouteBRouteImport.update({
+ id: '/route-b',
+ path: '/route-b',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
+ } as any)
+const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -234,25 +234,25 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/users': {
- id: '/users'
- path: '/users'
- fullPath: '/users'
- preLoaderRoute: typeof UsersRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/redirect': {
- id: '/redirect'
- path: '/redirect'
- fullPath: '/redirect'
- preLoaderRoute: typeof RedirectRouteImport
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/customScript.js': {
+ id: '/customScript.js'
+ path: '/customScript.js'
+ fullPath: '/customScript.js'
+ preLoaderRoute: typeof CustomScriptDotjsRouteImport
parentRoute: typeof rootRouteImport
}
'/deferred': {
@@ -262,33 +262,40 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
- '/customScript.js': {
- id: '/customScript.js'
- path: '/customScript.js'
- fullPath: '/customScript.js'
- preLoaderRoute: typeof CustomScriptDotjsRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ '/redirect': {
+ id: '/redirect'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRouteImport
}
- '/users/': {
- id: '/users/'
- path: '/'
- fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexRouteImport
- parentRoute: typeof UsersRoute
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
+ }
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRouteImport
}
'/posts/': {
id: '/posts/'
@@ -297,13 +304,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsIndexRouteImport
parentRoute: typeof PostsRoute
}
- '/users/$userId': {
- id: '/users/$userId'
- path: '/$userId'
- fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdRouteImport
- parentRoute: typeof UsersRoute
- }
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
@@ -311,33 +311,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/api/users': {
- id: '/api/users'
- path: '/api/users'
- fullPath: '/api/users'
- preLoaderRoute: typeof ApiUsersRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
- }
- '/posts_/$postId/deep': {
- id: '/posts_/$postId/deep'
- path: '/posts/$postId/deep'
- fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepRouteImport
- parentRoute: typeof rootRouteImport
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRoute
}
- '/api/users/$userId': {
- id: '/api/users/$userId'
+ '/users/$userId': {
+ id: '/users/$userId'
path: '/$userId'
- fullPath: '/api/users/$userId'
- preLoaderRoute: typeof ApiUsersUserIdRouteImport
- parentRoute: typeof ApiUsersRoute
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -346,12 +339,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
+ '/api/users/$userId': {
+ id: '/api/users/$userId'
+ path: '/$userId'
+ fullPath: '/api/users/$userId'
+ preLoaderRoute: typeof ApiUsersUserIdRouteImport
+ parentRoute: typeof ApiUsersRoute
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
}
}
}
diff --git a/examples/solid/start-basic-solid-query/package.json b/examples/solid/start-basic-solid-query/package.json
index e533f60dc7..c6c535c981 100644
--- a/examples/solid/start-basic-solid-query/package.json
+++ b/examples/solid/start-basic-solid-query/package.json
@@ -25,7 +25,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-basic-solid-query/src/routeTree.gen.ts b/examples/solid/start-basic-solid-query/src/routeTree.gen.ts
index 226b599b8c..4a1747ae36 100644
--- a/examples/solid/start-basic-solid-query/src/routeTree.gen.ts
+++ b/examples/solid/start-basic-solid-query/src/routeTree.gen.ts
@@ -9,26 +9,30 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as RedirectRouteImport } from './routes/redirect'
-import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
-import { Route as UsersRouteRouteImport } from './routes/users.route'
+import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as PostsRouteRouteImport } from './routes/posts.route'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as RedirectRouteImport } from './routes/redirect'
+import { Route as UsersRouteRouteImport } from './routes/users.route'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
+import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
-import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as ApiUsersRouteImport } from './routes/api/users'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-import { Route as ApiUsersIdRouteImport } from './routes/api/users.$id'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as ApiUsersIdRouteImport } from './routes/api/users.$id'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-const RedirectRoute = RedirectRouteImport.update({
- id: '/redirect',
- path: '/redirect',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
getParentRoute: () => rootRouteImport,
} as any)
const DeferredRoute = DeferredRouteImport.update({
@@ -36,8 +40,14 @@ const DeferredRoute = DeferredRouteImport.update({
path: '/deferred',
getParentRoute: () => rootRouteImport,
} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const PostsRouteRoute = PostsRouteRouteImport.update({
+ id: '/posts',
+ path: '/posts',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RedirectRoute = RedirectRouteImport.update({
+ id: '/redirect',
+ path: '/redirect',
getParentRoute: () => rootRouteImport,
} as any)
const UsersRouteRoute = UsersRouteRouteImport.update({
@@ -45,68 +55,58 @@ const UsersRouteRoute = UsersRouteRouteImport.update({
path: '/users',
getParentRoute: () => rootRouteImport,
} as any)
-const PostsRouteRoute = PostsRouteRouteImport.update({
- id: '/posts',
- path: '/posts',
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
+const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
- getParentRoute: () => rootRouteImport,
+ getParentRoute: () => PostsRouteRoute,
+} as any)
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRouteRoute,
} as any)
const UsersIndexRoute = UsersIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => UsersRouteRoute,
} as any)
-const PostsIndexRoute = PostsIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => PostsRouteRoute,
-} as any)
const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
getParentRoute: () => UsersRouteRoute,
} as any)
-const PostsPostIdRoute = PostsPostIdRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => PostsRouteRoute,
-} as any)
-const ApiUsersRoute = ApiUsersRouteImport.update({
- id: '/api/users',
- path: '/api/users',
- getParentRoute: () => rootRouteImport,
-} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
+const PathlessLayoutNestedLayoutRouteARoute =
+ PathlessLayoutNestedLayoutRouteARouteImport.update({
+ id: '/route-a',
+ path: '/route-a',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
- id: '/posts_/$postId/deep',
- path: '/posts/$postId/deep',
- getParentRoute: () => rootRouteImport,
-} as any)
-const ApiUsersIdRoute = ApiUsersIdRouteImport.update({
- id: '/$id',
- path: '/$id',
- getParentRoute: () => ApiUsersRoute,
-} as any)
const PathlessLayoutNestedLayoutRouteBRoute =
PathlessLayoutNestedLayoutRouteBRouteImport.update({
id: '/route-b',
path: '/route-b',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteARoute =
- PathlessLayoutNestedLayoutRouteARouteImport.update({
- id: '/route-a',
- path: '/route-a',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
+const ApiUsersIdRoute = ApiUsersIdRouteImport.update({
+ id: '/$id',
+ path: '/$id',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -221,18 +221,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/redirect': {
- id: '/redirect'
- path: '/redirect'
- fullPath: '/redirect'
- preLoaderRoute: typeof RedirectRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/deferred': {
- id: '/deferred'
- path: '/deferred'
- fullPath: '/deferred'
- preLoaderRoute: typeof DeferredRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_pathlessLayout': {
@@ -242,11 +235,11 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/users': {
- id: '/users'
- path: '/users'
- fullPath: '/users'
- preLoaderRoute: typeof UsersRouteRouteImport
+ '/deferred': {
+ id: '/deferred'
+ path: '/deferred'
+ fullPath: '/deferred'
+ preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
'/posts': {
@@ -256,19 +249,33 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsRouteRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/redirect': {
+ id: '/redirect'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectRouteImport
parentRoute: typeof rootRouteImport
}
- '/users/': {
- id: '/users/'
- path: '/'
- fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexRouteImport
- parentRoute: typeof UsersRouteRoute
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
+ }
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRouteImport
}
'/posts/': {
id: '/posts/'
@@ -277,13 +284,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsIndexRouteImport
parentRoute: typeof PostsRouteRoute
}
- '/users/$userId': {
- id: '/users/$userId'
- path: '/$userId'
- fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdRouteImport
- parentRoute: typeof UsersRouteRoute
- }
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
@@ -291,33 +291,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRouteRoute
}
- '/api/users': {
- id: '/api/users'
- path: '/api/users'
- fullPath: '/api/users'
- preLoaderRoute: typeof ApiUsersRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRouteRoute
}
- '/posts_/$postId/deep': {
- id: '/posts_/$postId/deep'
- path: '/posts/$postId/deep'
- fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepRouteImport
- parentRoute: typeof rootRouteImport
+ '/users/$userId': {
+ id: '/users/$userId'
+ path: '/$userId'
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRouteRoute
}
- '/api/users/$id': {
- id: '/api/users/$id'
- path: '/$id'
- fullPath: '/api/users/$id'
- preLoaderRoute: typeof ApiUsersIdRouteImport
- parentRoute: typeof ApiUsersRoute
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -326,12 +319,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
+ '/api/users/$id': {
+ id: '/api/users/$id'
+ path: '/$id'
+ fullPath: '/api/users/$id'
+ preLoaderRoute: typeof ApiUsersIdRouteImport
+ parentRoute: typeof ApiUsersRoute
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
}
}
}
diff --git a/examples/solid/start-basic-static/package.json b/examples/solid/start-basic-static/package.json
index c56ee0f4be..31f3a1d6dc 100644
--- a/examples/solid/start-basic-static/package.json
+++ b/examples/solid/start-basic-static/package.json
@@ -14,7 +14,7 @@
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"@tanstack/solid-start": "^2.0.0-beta.30",
- "@tanstack/start-static-server-functions": "^1.167.18",
+ "@tanstack/start-static-server-functions": "^1.167.20",
"redaxios": "^0.5.1",
"solid-js": "2.0.0-beta.29",
"tailwind-merge": "^3.6.0"
@@ -23,7 +23,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.3"
diff --git a/examples/solid/start-basic-static/src/routeTree.gen.ts b/examples/solid/start-basic-static/src/routeTree.gen.ts
index 7cad5523aa..a94e0fcf2e 100644
--- a/examples/solid/start-basic-static/src/routeTree.gen.ts
+++ b/examples/solid/start-basic-static/src/routeTree.gen.ts
@@ -9,29 +9,33 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as UsersRouteImport } from './routes/users'
-import { Route as RedirectRouteImport } from './routes/redirect'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as DeferredRouteImport } from './routes/deferred'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RedirectRouteImport } from './routes/redirect'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
-import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-const UsersRoute = UsersRouteImport.update({
- id: '/users',
- path: '/users',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const RedirectRoute = RedirectRouteImport.update({
- id: '/redirect',
- path: '/redirect',
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const DeferredRoute = DeferredRouteImport.update({
+ id: '/deferred',
+ path: '/deferred',
getParentRoute: () => rootRouteImport,
} as any)
const PostsRoute = PostsRouteImport.update({
@@ -39,62 +43,58 @@ const PostsRoute = PostsRouteImport.update({
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const DeferredRoute = DeferredRouteImport.update({
- id: '/deferred',
- path: '/deferred',
+const RedirectRoute = RedirectRouteImport.update({
+ id: '/redirect',
+ path: '/redirect',
getParentRoute: () => rootRouteImport,
} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const UsersRoute = UsersRouteImport.update({
+ id: '/users',
+ path: '/users',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
+const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
- getParentRoute: () => rootRouteImport,
+ getParentRoute: () => PostsRoute,
+} as any)
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
} as any)
const UsersIndexRoute = UsersIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => UsersRoute,
} as any)
-const PostsIndexRoute = PostsIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => PostsRoute,
-} as any)
const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
-const PostsPostIdRoute = PostsPostIdRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => PostsRoute,
-} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
+const PathlessLayoutNestedLayoutRouteARoute =
+ PathlessLayoutNestedLayoutRouteARouteImport.update({
+ id: '/route-a',
+ path: '/route-a',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
- id: '/posts_/$postId/deep',
- path: '/posts/$postId/deep',
- getParentRoute: () => rootRouteImport,
-} as any)
const PathlessLayoutNestedLayoutRouteBRoute =
PathlessLayoutNestedLayoutRouteBRouteImport.update({
id: '/route-b',
path: '/route-b',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteARoute =
- PathlessLayoutNestedLayoutRouteARouteImport.update({
- id: '/route-a',
- path: '/route-a',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -196,18 +196,25 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/users': {
- id: '/users'
- path: '/users'
- fullPath: '/users'
- preLoaderRoute: typeof UsersRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/redirect': {
- id: '/redirect'
- path: '/redirect'
- fullPath: '/redirect'
- preLoaderRoute: typeof RedirectRouteImport
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/deferred': {
+ id: '/deferred'
+ path: '/deferred'
+ fullPath: '/deferred'
+ preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
'/posts': {
@@ -217,33 +224,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/deferred': {
- id: '/deferred'
- path: '/deferred'
- fullPath: '/deferred'
- preLoaderRoute: typeof DeferredRouteImport
+ '/redirect': {
+ id: '/redirect'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectRouteImport
parentRoute: typeof rootRouteImport
}
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/users/': {
- id: '/users/'
- path: '/'
- fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexRouteImport
- parentRoute: typeof UsersRoute
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
}
'/posts/': {
id: '/posts/'
@@ -252,13 +252,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsIndexRouteImport
parentRoute: typeof PostsRoute
}
- '/users/$userId': {
- id: '/users/$userId'
- path: '/$userId'
- fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdRouteImport
- parentRoute: typeof UsersRoute
- }
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
@@ -266,19 +259,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRoute
}
- '/posts_/$postId/deep': {
- id: '/posts_/$postId/deep'
- path: '/posts/$postId/deep'
- fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepRouteImport
- parentRoute: typeof rootRouteImport
+ '/users/$userId': {
+ id: '/users/$userId'
+ path: '/$userId'
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -287,12 +287,12 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
}
}
}
diff --git a/examples/solid/start-basic/package.json b/examples/solid/start-basic/package.json
index 07568247ec..d3d3c3ace9 100644
--- a/examples/solid/start-basic/package.json
+++ b/examples/solid/start-basic/package.json
@@ -23,7 +23,8 @@
"@types/node": "^22.5.4",
"nitro": "^3.0.260311-beta",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-basic/src/routeTree.gen.ts b/examples/solid/start-basic/src/routeTree.gen.ts
index 722133b41e..8d6c8ffc2b 100644
--- a/examples/solid/start-basic/src/routeTree.gen.ts
+++ b/examples/solid/start-basic/src/routeTree.gen.ts
@@ -9,31 +9,35 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as UsersRouteImport } from './routes/users'
-import { Route as RedirectRouteImport } from './routes/redirect'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as DeferredRouteImport } from './routes/deferred'
-import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
+import { Route as DeferredRouteImport } from './routes/deferred'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RedirectRouteImport } from './routes/redirect'
+import { Route as UsersRouteImport } from './routes/users'
+import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
+import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
-import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as ApiUsersRouteImport } from './routes/api/users'
-import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout'
-import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
-import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as UsersIndexRouteImport } from './routes/users.index'
+import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a'
+import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b'
+import { Route as ApiUsersUserIdRouteImport } from './routes/api/users.$userId'
+import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
-const UsersRoute = UsersRouteImport.update({
- id: '/users',
- path: '/users',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const RedirectRoute = RedirectRouteImport.update({
- id: '/redirect',
- path: '/redirect',
+const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
+ id: '/_pathlessLayout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const DeferredRoute = DeferredRouteImport.update({
+ id: '/deferred',
+ path: '/deferred',
getParentRoute: () => rootRouteImport,
} as any)
const PostsRoute = PostsRouteImport.update({
@@ -41,72 +45,68 @@ const PostsRoute = PostsRouteImport.update({
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const DeferredRoute = DeferredRouteImport.update({
- id: '/deferred',
- path: '/deferred',
+const RedirectRoute = RedirectRouteImport.update({
+ id: '/redirect',
+ path: '/redirect',
getParentRoute: () => rootRouteImport,
} as any)
-const PathlessLayoutRoute = PathlessLayoutRouteImport.update({
- id: '/_pathlessLayout',
+const UsersRoute = UsersRouteImport.update({
+ id: '/users',
+ path: '/users',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const PathlessLayoutNestedLayoutRoute =
+ PathlessLayoutNestedLayoutRouteImport.update({
+ id: '/_nested-layout',
+ getParentRoute: () => PathlessLayoutRoute,
+ } as any)
+const ApiUsersRoute = ApiUsersRouteImport.update({
+ id: '/api/users',
+ path: '/api/users',
getParentRoute: () => rootRouteImport,
} as any)
-const UsersIndexRoute = UsersIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => UsersRoute,
-} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PostsRoute,
} as any)
-const UsersUserIdRoute = UsersUserIdRouteImport.update({
- id: '/$userId',
- path: '/$userId',
- getParentRoute: () => UsersRoute,
-} as any)
const PostsPostIdRoute = PostsPostIdRouteImport.update({
id: '/$postId',
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const ApiUsersRoute = ApiUsersRouteImport.update({
- id: '/api/users',
- path: '/api/users',
- getParentRoute: () => rootRouteImport,
-} as any)
-const PathlessLayoutNestedLayoutRoute =
- PathlessLayoutNestedLayoutRouteImport.update({
- id: '/_nested-layout',
- getParentRoute: () => PathlessLayoutRoute,
- } as any)
-const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
- id: '/posts_/$postId/deep',
- path: '/posts/$postId/deep',
- getParentRoute: () => rootRouteImport,
+const UsersIndexRoute = UsersIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => UsersRoute,
} as any)
-const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+const UsersUserIdRoute = UsersUserIdRouteImport.update({
id: '/$userId',
path: '/$userId',
- getParentRoute: () => ApiUsersRoute,
+ getParentRoute: () => UsersRoute,
} as any)
-const PathlessLayoutNestedLayoutRouteBRoute =
- PathlessLayoutNestedLayoutRouteBRouteImport.update({
- id: '/route-b',
- path: '/route-b',
- getParentRoute: () => PathlessLayoutNestedLayoutRoute,
- } as any)
const PathlessLayoutNestedLayoutRouteARoute =
PathlessLayoutNestedLayoutRouteARouteImport.update({
id: '/route-a',
path: '/route-a',
getParentRoute: () => PathlessLayoutNestedLayoutRoute,
} as any)
+const PathlessLayoutNestedLayoutRouteBRoute =
+ PathlessLayoutNestedLayoutRouteBRouteImport.update({
+ id: '/route-b',
+ path: '/route-b',
+ getParentRoute: () => PathlessLayoutNestedLayoutRoute,
+ } as any)
+const ApiUsersUserIdRoute = ApiUsersUserIdRouteImport.update({
+ id: '/$userId',
+ path: '/$userId',
+ getParentRoute: () => ApiUsersRoute,
+} as any)
+const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
+ id: '/posts_/$postId/deep',
+ path: '/posts/$postId/deep',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -221,18 +221,25 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/users': {
- id: '/users'
- path: '/users'
- fullPath: '/users'
- preLoaderRoute: typeof UsersRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/redirect': {
- id: '/redirect'
- path: '/redirect'
- fullPath: '/redirect'
- preLoaderRoute: typeof RedirectRouteImport
+ '/_pathlessLayout': {
+ id: '/_pathlessLayout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof PathlessLayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/deferred': {
+ id: '/deferred'
+ path: '/deferred'
+ fullPath: '/deferred'
+ preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
'/posts': {
@@ -242,33 +249,33 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/deferred': {
- id: '/deferred'
- path: '/deferred'
- fullPath: '/deferred'
- preLoaderRoute: typeof DeferredRouteImport
+ '/redirect': {
+ id: '/redirect'
+ path: '/redirect'
+ fullPath: '/redirect'
+ preLoaderRoute: typeof RedirectRouteImport
parentRoute: typeof rootRouteImport
}
- '/_pathlessLayout': {
- id: '/_pathlessLayout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutRouteImport
+ '/users': {
+ id: '/users'
+ path: '/users'
+ fullPath: '/users'
+ preLoaderRoute: typeof UsersRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
+ '/_pathlessLayout/_nested-layout': {
+ id: '/_pathlessLayout/_nested-layout'
+ path: ''
fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
+ parentRoute: typeof PathlessLayoutRoute
}
- '/users/': {
- id: '/users/'
- path: '/'
- fullPath: '/users/'
- preLoaderRoute: typeof UsersIndexRouteImport
- parentRoute: typeof UsersRoute
+ '/api/users': {
+ id: '/api/users'
+ path: '/api/users'
+ fullPath: '/api/users'
+ preLoaderRoute: typeof ApiUsersRouteImport
+ parentRoute: typeof rootRouteImport
}
'/posts/': {
id: '/posts/'
@@ -277,13 +284,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsIndexRouteImport
parentRoute: typeof PostsRoute
}
- '/users/$userId': {
- id: '/users/$userId'
- path: '/$userId'
- fullPath: '/users/$userId'
- preLoaderRoute: typeof UsersUserIdRouteImport
- parentRoute: typeof UsersRoute
- }
'/posts/$postId': {
id: '/posts/$postId'
path: '/$postId'
@@ -291,33 +291,26 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/api/users': {
- id: '/api/users'
- path: '/api/users'
- fullPath: '/api/users'
- preLoaderRoute: typeof ApiUsersRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/_pathlessLayout/_nested-layout': {
- id: '/_pathlessLayout/_nested-layout'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteImport
- parentRoute: typeof PathlessLayoutRoute
- }
- '/posts_/$postId/deep': {
- id: '/posts_/$postId/deep'
- path: '/posts/$postId/deep'
- fullPath: '/posts/$postId/deep'
- preLoaderRoute: typeof PostsPostIdDeepRouteImport
- parentRoute: typeof rootRouteImport
+ '/users/': {
+ id: '/users/'
+ path: '/'
+ fullPath: '/users/'
+ preLoaderRoute: typeof UsersIndexRouteImport
+ parentRoute: typeof UsersRoute
}
- '/api/users/$userId': {
- id: '/api/users/$userId'
+ '/users/$userId': {
+ id: '/users/$userId'
path: '/$userId'
- fullPath: '/api/users/$userId'
- preLoaderRoute: typeof ApiUsersUserIdRouteImport
- parentRoute: typeof ApiUsersRoute
+ fullPath: '/users/$userId'
+ preLoaderRoute: typeof UsersUserIdRouteImport
+ parentRoute: typeof UsersRoute
+ }
+ '/_pathlessLayout/_nested-layout/route-a': {
+ id: '/_pathlessLayout/_nested-layout/route-a'
+ path: '/route-a'
+ fullPath: '/route-a'
+ preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
+ parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
'/_pathlessLayout/_nested-layout/route-b': {
id: '/_pathlessLayout/_nested-layout/route-b'
@@ -326,12 +319,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBRouteImport
parentRoute: typeof PathlessLayoutNestedLayoutRoute
}
- '/_pathlessLayout/_nested-layout/route-a': {
- id: '/_pathlessLayout/_nested-layout/route-a'
- path: '/route-a'
- fullPath: '/route-a'
- preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteARouteImport
- parentRoute: typeof PathlessLayoutNestedLayoutRoute
+ '/api/users/$userId': {
+ id: '/api/users/$userId'
+ path: '/$userId'
+ fullPath: '/api/users/$userId'
+ preLoaderRoute: typeof ApiUsersUserIdRouteImport
+ parentRoute: typeof ApiUsersRoute
+ }
+ '/posts_/$postId/deep': {
+ id: '/posts_/$postId/deep'
+ path: '/posts/$postId/deep'
+ fullPath: '/posts/$postId/deep'
+ preLoaderRoute: typeof PostsPostIdDeepRouteImport
+ parentRoute: typeof rootRouteImport
}
}
}
diff --git a/examples/solid/start-bun/package.json b/examples/solid/start-bun/package.json
index 1573ed8b86..73bc4e8e7b 100644
--- a/examples/solid/start-bun/package.json
+++ b/examples/solid/start-bun/package.json
@@ -15,7 +15,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-devtools": "^0.7.0",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
@@ -32,7 +32,8 @@
"@types/node": "22.10.2",
"jsdom": "^27.0.0",
"prettier": "^3.6.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vitest": "^4.1.4",
diff --git a/examples/solid/start-bun/src/routeTree.gen.ts b/examples/solid/start-bun/src/routeTree.gen.ts
index 19009b2f5b..353db2cd02 100644
--- a/examples/solid/start-bun/src/routeTree.gen.ts
+++ b/examples/solid/start-bun/src/routeTree.gen.ts
@@ -11,8 +11,8 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiDemoNamesRouteImport } from './routes/api.demo-names'
-import { Route as DemoStartServerFuncsRouteImport } from './routes/demo.start.server-funcs'
import { Route as DemoStartApiRequestRouteImport } from './routes/demo.start.api-request'
+import { Route as DemoStartServerFuncsRouteImport } from './routes/demo.start.server-funcs'
const IndexRoute = IndexRouteImport.update({
id: '/',
@@ -24,16 +24,16 @@ const ApiDemoNamesRoute = ApiDemoNamesRouteImport.update({
path: '/api/demo-names',
getParentRoute: () => rootRouteImport,
} as any)
-const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
- id: '/demo/start/server-funcs',
- path: '/demo/start/server-funcs',
- getParentRoute: () => rootRouteImport,
-} as any)
const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({
id: '/demo/start/api-request',
path: '/demo/start/api-request',
getParentRoute: () => rootRouteImport,
} as any)
+const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
+ id: '/demo/start/server-funcs',
+ path: '/demo/start/server-funcs',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -98,13 +98,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ApiDemoNamesRouteImport
parentRoute: typeof rootRouteImport
}
- '/demo/start/server-funcs': {
- id: '/demo/start/server-funcs'
- path: '/demo/start/server-funcs'
- fullPath: '/demo/start/server-funcs'
- preLoaderRoute: typeof DemoStartServerFuncsRouteImport
- parentRoute: typeof rootRouteImport
- }
'/demo/start/api-request': {
id: '/demo/start/api-request'
path: '/demo/start/api-request'
@@ -112,6 +105,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof DemoStartApiRequestRouteImport
parentRoute: typeof rootRouteImport
}
+ '/demo/start/server-funcs': {
+ id: '/demo/start/server-funcs'
+ path: '/demo/start/server-funcs'
+ fullPath: '/demo/start/server-funcs'
+ preLoaderRoute: typeof DemoStartServerFuncsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/start-convex-better-auth/package.json b/examples/solid/start-convex-better-auth/package.json
index a7b4ecec69..48eb2206c5 100644
--- a/examples/solid/start-convex-better-auth/package.json
+++ b/examples/solid/start-convex-better-auth/package.json
@@ -17,7 +17,7 @@
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"@tanstack/solid-start": "^2.0.0-beta.30",
- "better-auth": "^1.3.27",
+ "better-auth": "^1.6.19",
"clsx": "^2.1.1",
"convex": "^1.28.2",
"convex-solidjs": "^0.0.3",
@@ -30,7 +30,8 @@
"devDependencies": {
"@types/node": "^22.10.2",
"combinate": "^1.1.11",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-convex-better-auth/src/routeTree.gen.ts b/examples/solid/start-convex-better-auth/src/routeTree.gen.ts
index 27dc0ca88c..d2307d1277 100644
--- a/examples/solid/start-convex-better-auth/src/routeTree.gen.ts
+++ b/examples/solid/start-convex-better-auth/src/routeTree.gen.ts
@@ -9,24 +9,24 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AboutRouteImport } from './routes/about'
-import { Route as AuthedRouteImport } from './routes/_authed'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AuthedRouteImport } from './routes/_authed'
+import { Route as AboutRouteImport } from './routes/about'
import { Route as AuthedDashboardRouteImport } from './routes/_authed/dashboard'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
-const AboutRoute = AboutRouteImport.update({
- id: '/about',
- path: '/about',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedRoute = AuthedRouteImport.update({
id: '/_authed',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedDashboardRoute = AuthedDashboardRouteImport.update({
@@ -83,11 +83,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/about': {
- id: '/about'
- path: '/about'
- fullPath: '/about'
- preLoaderRoute: typeof AboutRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed': {
@@ -97,11 +97,11 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AuthedRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/dashboard': {
diff --git a/examples/solid/start-counter/package.json b/examples/solid/start-counter/package.json
index daeea04b17..9adf93e962 100644
--- a/examples/solid/start-counter/package.json
+++ b/examples/solid/start-counter/package.json
@@ -22,7 +22,8 @@
"devDependencies": {
"@types/node": "^22.10.2",
"combinate": "^1.1.11",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/start-counter/src/routeTree.gen.ts b/examples/solid/start-counter/src/routeTree.gen.ts
index 3cc45c071e..7495244535 100644
--- a/examples/solid/start-counter/src/routeTree.gen.ts
+++ b/examples/solid/start-counter/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AboutRouteImport } from './routes/about'
-const AboutRoute = AboutRouteImport.update({
- id: '/about',
- path: '/about',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/about': {
- id: '/about'
- path: '/about'
- fullPath: '/about'
- preLoaderRoute: typeof AboutRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/start-i18n-paraglide/package.json b/examples/solid/start-i18n-paraglide/package.json
index 13d8940d99..0216a57551 100644
--- a/examples/solid/start-i18n-paraglide/package.json
+++ b/examples/solid/start-i18n-paraglide/package.json
@@ -21,7 +21,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.18.6",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/start-i18n-paraglide/src/routeTree.gen.ts b/examples/solid/start-i18n-paraglide/src/routeTree.gen.ts
index 3cc45c071e..7495244535 100644
--- a/examples/solid/start-i18n-paraglide/src/routeTree.gen.ts
+++ b/examples/solid/start-i18n-paraglide/src/routeTree.gen.ts
@@ -9,19 +9,19 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AboutRouteImport } from './routes/about'
-const AboutRoute = AboutRouteImport.update({
- id: '/about',
- path: '/about',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -51,13 +51,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/about': {
- id: '/about'
- path: '/about'
- fullPath: '/about'
- preLoaderRoute: typeof AboutRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -65,6 +58,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
diff --git a/examples/solid/start-large/package.json b/examples/solid/start-large/package.json
index e4b08153ad..ca4fb37709 100644
--- a/examples/solid/start-large/package.json
+++ b/examples/solid/start-large/package.json
@@ -26,7 +26,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
},
diff --git a/examples/solid/start-large/src/routeTree.gen.ts b/examples/solid/start-large/src/routeTree.gen.ts
index 3fb99e1aca..1febe90621 100644
--- a/examples/solid/start-large/src/routeTree.gen.ts
+++ b/examples/solid/start-large/src/routeTree.gen.ts
@@ -9,23 +9,18 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as RelativeRouteImport } from './routes/relative'
-import { Route as LinkPropsRouteImport } from './routes/linkProps'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as AbsoluteRouteImport } from './routes/absolute'
-import { Route as SearchRouteRouteImport } from './routes/search/route'
+import { Route as LinkPropsRouteImport } from './routes/linkProps'
import { Route as ParamsRouteRouteImport } from './routes/params/route'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as SearchSearchPlaceholderRouteImport } from './routes/search/searchPlaceholder'
+import { Route as RelativeRouteImport } from './routes/relative'
+import { Route as SearchRouteRouteImport } from './routes/search/route'
import { Route as ParamsParamsPlaceholderRouteImport } from './routes/params/$paramsPlaceholder'
+import { Route as SearchSearchPlaceholderRouteImport } from './routes/search/searchPlaceholder'
-const RelativeRoute = RelativeRouteImport.update({
- id: '/relative',
- path: '/relative',
- getParentRoute: () => rootRouteImport,
-} as any)
-const LinkPropsRoute = LinkPropsRouteImport.update({
- id: '/linkProps',
- path: '/linkProps',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AbsoluteRoute = AbsoluteRouteImport.update({
@@ -33,9 +28,9 @@ const AbsoluteRoute = AbsoluteRouteImport.update({
path: '/absolute',
getParentRoute: () => rootRouteImport,
} as any)
-const SearchRouteRoute = SearchRouteRouteImport.update({
- id: '/search',
- path: '/search',
+const LinkPropsRoute = LinkPropsRouteImport.update({
+ id: '/linkProps',
+ path: '/linkProps',
getParentRoute: () => rootRouteImport,
} as any)
const ParamsRouteRoute = ParamsRouteRouteImport.update({
@@ -43,21 +38,26 @@ const ParamsRouteRoute = ParamsRouteRouteImport.update({
path: '/params',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const RelativeRoute = RelativeRouteImport.update({
+ id: '/relative',
+ path: '/relative',
getParentRoute: () => rootRouteImport,
} as any)
-const SearchSearchPlaceholderRoute = SearchSearchPlaceholderRouteImport.update({
- id: '/searchPlaceholder',
- path: '/searchPlaceholder',
- getParentRoute: () => SearchRouteRoute,
+const SearchRouteRoute = SearchRouteRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => rootRouteImport,
} as any)
const ParamsParamsPlaceholderRoute = ParamsParamsPlaceholderRouteImport.update({
id: '/$paramsPlaceholder',
path: '/$paramsPlaceholder',
getParentRoute: () => ParamsRouteRoute,
} as any)
+const SearchSearchPlaceholderRoute = SearchSearchPlaceholderRouteImport.update({
+ id: '/searchPlaceholder',
+ path: '/searchPlaceholder',
+ getParentRoute: () => SearchRouteRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -134,18 +134,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/relative': {
- id: '/relative'
- path: '/relative'
- fullPath: '/relative'
- preLoaderRoute: typeof RelativeRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/linkProps': {
- id: '/linkProps'
- path: '/linkProps'
- fullPath: '/linkProps'
- preLoaderRoute: typeof LinkPropsRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/absolute': {
@@ -155,11 +148,11 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof AbsoluteRouteImport
parentRoute: typeof rootRouteImport
}
- '/search': {
- id: '/search'
- path: '/search'
- fullPath: '/search'
- preLoaderRoute: typeof SearchRouteRouteImport
+ '/linkProps': {
+ id: '/linkProps'
+ path: '/linkProps'
+ fullPath: '/linkProps'
+ preLoaderRoute: typeof LinkPropsRouteImport
parentRoute: typeof rootRouteImport
}
'/params': {
@@ -169,19 +162,19 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ParamsRouteRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/relative': {
+ id: '/relative'
+ path: '/relative'
+ fullPath: '/relative'
+ preLoaderRoute: typeof RelativeRouteImport
parentRoute: typeof rootRouteImport
}
- '/search/searchPlaceholder': {
- id: '/search/searchPlaceholder'
- path: '/searchPlaceholder'
- fullPath: '/search/searchPlaceholder'
- preLoaderRoute: typeof SearchSearchPlaceholderRouteImport
- parentRoute: typeof SearchRouteRoute
+ '/search': {
+ id: '/search'
+ path: '/search'
+ fullPath: '/search'
+ preLoaderRoute: typeof SearchRouteRouteImport
+ parentRoute: typeof rootRouteImport
}
'/params/$paramsPlaceholder': {
id: '/params/$paramsPlaceholder'
@@ -190,6 +183,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ParamsParamsPlaceholderRouteImport
parentRoute: typeof ParamsRouteRoute
}
+ '/search/searchPlaceholder': {
+ id: '/search/searchPlaceholder'
+ path: '/searchPlaceholder'
+ fullPath: '/search/searchPlaceholder'
+ preLoaderRoute: typeof SearchSearchPlaceholderRouteImport
+ parentRoute: typeof SearchRouteRoute
+ }
}
}
diff --git a/examples/solid/start-streaming-data-from-server-functions/package.json b/examples/solid/start-streaming-data-from-server-functions/package.json
index 25c8ec40d3..4478ba9c18 100644
--- a/examples/solid/start-streaming-data-from-server-functions/package.json
+++ b/examples/solid/start-streaming-data-from-server-functions/package.json
@@ -19,7 +19,8 @@
},
"devDependencies": {
"@types/node": "^22.5.4",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/start-supabase-basic/package.json b/examples/solid/start-supabase-basic/package.json
index c81e5c6206..bf190803d2 100644
--- a/examples/solid/start-supabase-basic/package.json
+++ b/examples/solid/start-supabase-basic/package.json
@@ -25,7 +25,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/start-supabase-basic/src/routeTree.gen.ts b/examples/solid/start-supabase-basic/src/routeTree.gen.ts
index 7019a6c21a..bbebb19df3 100644
--- a/examples/solid/start-supabase-basic/src/routeTree.gen.ts
+++ b/examples/solid/start-supabase-basic/src/routeTree.gen.ts
@@ -9,23 +9,22 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as SignupRouteImport } from './routes/signup'
-import { Route as LogoutRouteImport } from './routes/logout'
-import { Route as LoginRouteImport } from './routes/login'
-import { Route as AuthedRouteImport } from './routes/_authed'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AuthedRouteImport } from './routes/_authed'
+import { Route as LoginRouteImport } from './routes/login'
+import { Route as LogoutRouteImport } from './routes/logout'
+import { Route as SignupRouteImport } from './routes/signup'
import { Route as AuthedPostsRouteImport } from './routes/_authed/posts'
import { Route as AuthedPostsIndexRouteImport } from './routes/_authed/posts.index'
import { Route as AuthedPostsPostIdRouteImport } from './routes/_authed/posts.$postId'
-const SignupRoute = SignupRouteImport.update({
- id: '/signup',
- path: '/signup',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const LogoutRoute = LogoutRouteImport.update({
- id: '/logout',
- path: '/logout',
+const AuthedRoute = AuthedRouteImport.update({
+ id: '/_authed',
getParentRoute: () => rootRouteImport,
} as any)
const LoginRoute = LoginRouteImport.update({
@@ -33,13 +32,14 @@ const LoginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
-const AuthedRoute = AuthedRouteImport.update({
- id: '/_authed',
+const LogoutRoute = LogoutRouteImport.update({
+ id: '/logout',
+ path: '/logout',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const SignupRoute = SignupRouteImport.update({
+ id: '/signup',
+ path: '/signup',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedPostsRoute = AuthedPostsRouteImport.update({
@@ -120,18 +120,18 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/signup': {
- id: '/signup'
- path: '/signup'
- fullPath: '/signup'
- preLoaderRoute: typeof SignupRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/logout': {
- id: '/logout'
- path: '/logout'
- fullPath: '/logout'
- preLoaderRoute: typeof LogoutRouteImport
+ '/_authed': {
+ id: '/_authed'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof AuthedRouteImport
parentRoute: typeof rootRouteImport
}
'/login': {
@@ -141,18 +141,18 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
- '/_authed': {
- id: '/_authed'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof AuthedRouteImport
+ '/logout': {
+ id: '/logout'
+ path: '/logout'
+ fullPath: '/logout'
+ preLoaderRoute: typeof LogoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/signup': {
+ id: '/signup'
+ path: '/signup'
+ fullPath: '/signup'
+ preLoaderRoute: typeof SignupRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/posts': {
diff --git a/examples/solid/start-tailwind-v4/package.json b/examples/solid/start-tailwind-v4/package.json
index 01121682da..0250e91d97 100644
--- a/examples/solid/start-tailwind-v4/package.json
+++ b/examples/solid/start-tailwind-v4/package.json
@@ -22,7 +22,8 @@
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22.5.4",
"tailwindcss": "^4.2.2",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/examples/solid/view-transitions/package.json b/examples/solid/view-transitions/package.json
index 31f316f125..08f60be2c8 100644
--- a/examples/solid/view-transitions/package.json
+++ b/examples/solid/view-transitions/package.json
@@ -11,7 +11,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"redaxios": "^0.5.1",
@@ -20,7 +20,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/view-transitions/src/routeTree.gen.ts b/examples/solid/view-transitions/src/routeTree.gen.ts
index 114e34ed9d..01843027dc 100644
--- a/examples/solid/view-transitions/src/routeTree.gen.ts
+++ b/examples/solid/view-transitions/src/routeTree.gen.ts
@@ -9,16 +9,16 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as HowItWorksRouteImport } from './routes/how-it-works'
+import { Route as IndexRouteImport } from './routes/index'
import { Route as ExploreRouteImport } from './routes/explore'
+import { Route as HowItWorksRouteImport } from './routes/how-it-works'
import { Route as PostsRouteRouteImport } from './routes/posts.route'
-import { Route as IndexRouteImport } from './routes/index'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-const HowItWorksRoute = HowItWorksRouteImport.update({
- id: '/how-it-works',
- path: '/how-it-works',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const ExploreRoute = ExploreRouteImport.update({
@@ -26,16 +26,16 @@ const ExploreRoute = ExploreRouteImport.update({
path: '/explore',
getParentRoute: () => rootRouteImport,
} as any)
+const HowItWorksRoute = HowItWorksRouteImport.update({
+ id: '/how-it-works',
+ path: '/how-it-works',
+ getParentRoute: () => rootRouteImport,
+} as any)
const PostsRouteRoute = PostsRouteRouteImport.update({
id: '/posts',
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => rootRouteImport,
-} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
@@ -101,11 +101,11 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/how-it-works': {
- id: '/how-it-works'
- path: '/how-it-works'
- fullPath: '/how-it-works'
- preLoaderRoute: typeof HowItWorksRouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/explore': {
@@ -115,6 +115,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ExploreRouteImport
parentRoute: typeof rootRouteImport
}
+ '/how-it-works': {
+ id: '/how-it-works'
+ path: '/how-it-works'
+ fullPath: '/how-it-works'
+ preLoaderRoute: typeof HowItWorksRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/posts': {
id: '/posts'
path: '/posts'
@@ -122,13 +129,6 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof PostsRouteRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
- }
'/posts/': {
id: '/posts/'
path: '/'
diff --git a/examples/solid/with-framer-motion/package.json b/examples/solid/with-framer-motion/package.json
index 501bf2ded9..2e3705c571 100644
--- a/examples/solid/with-framer-motion/package.json
+++ b/examples/solid/with-framer-motion/package.json
@@ -20,7 +20,8 @@
"zod": "^4.4.3"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
"vite-plugin-solid": "^3.0.0-next.21"
}
diff --git a/examples/solid/with-trpc/package.json b/examples/solid/with-trpc/package.json
index 858892f717..030e917050 100644
--- a/examples/solid/with-trpc/package.json
+++ b/examples/solid/with-trpc/package.json
@@ -12,7 +12,7 @@
"dependencies": {
"@solidjs/web": "2.0.0-beta.29",
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
+ "@tanstack/router-plugin": "^1.168.24",
"@tanstack/solid-router": "^2.0.0-beta.29",
"@tanstack/solid-router-devtools": "^2.0.0-beta.24",
"@trpc/client": "^11.4.3",
diff --git a/examples/solid/with-trpc/src/routeTree.gen.ts b/examples/solid/with-trpc/src/routeTree.gen.ts
index fd93507694..aa08570aca 100644
--- a/examples/solid/with-trpc/src/routeTree.gen.ts
+++ b/examples/solid/with-trpc/src/routeTree.gen.ts
@@ -9,23 +9,23 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as DashboardRouteImport } from './routes/dashboard'
import { Route as DashboardIndexRouteImport } from './routes/dashboard.index'
import { Route as DashboardPostsRouteImport } from './routes/dashboard.posts'
import { Route as DashboardPostsIndexRouteImport } from './routes/dashboard.posts.index'
import { Route as DashboardPostsPostIdRouteImport } from './routes/dashboard.posts.$postId'
-const DashboardRoute = DashboardRouteImport.update({
- id: '/dashboard',
- path: '/dashboard',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const DashboardRoute = DashboardRouteImport.update({
+ id: '/dashboard',
+ path: '/dashboard',
+ getParentRoute: () => rootRouteImport,
+} as any)
const DashboardIndexRoute = DashboardIndexRouteImport.update({
id: '/',
path: '/',
@@ -98,13 +98,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/dashboard': {
- id: '/dashboard'
- path: '/dashboard'
- fullPath: '/dashboard'
- preLoaderRoute: typeof DashboardRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -112,6 +105,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/dashboard': {
+ id: '/dashboard'
+ path: '/dashboard'
+ fullPath: '/dashboard'
+ preLoaderRoute: typeof DashboardRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/dashboard/': {
id: '/dashboard/'
path: '/'
diff --git a/examples/vue/basic-file-based-jsx/package.json b/examples/vue/basic-file-based-jsx/package.json
index 7573628b17..5e94ab51ea 100644
--- a/examples/vue/basic-file-based-jsx/package.json
+++ b/examples/vue/basic-file-based-jsx/package.json
@@ -10,9 +10,9 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
- "@tanstack/vue-router": "^1.170.16",
- "@tanstack/vue-router-devtools": "^1.167.0",
+ "@tanstack/router-plugin": "^1.168.24",
+ "@tanstack/vue-router": "^1.170.18",
+ "@tanstack/vue-router-devtools": "^1.167.1",
"redaxios": "^0.5.1",
"tailwindcss": "^4.2.2",
"vue": "^3.5.16",
@@ -28,6 +28,6 @@
"typescript": "~5.8.3",
"vite": "^8.0.14",
"vue-eslint-parser": "^9.4.3",
- "vue-tsc": "^3.1.5"
+ "vue-tsc": "^3.3.8"
}
}
diff --git a/examples/vue/basic-file-based-jsx/src/routeTree.gen.ts b/examples/vue/basic-file-based-jsx/src/routeTree.gen.ts
index f5cd519d7f..3922025c82 100644
--- a/examples/vue/basic-file-based-jsx/src/routeTree.gen.ts
+++ b/examples/vue/basic-file-based-jsx/src/routeTree.gen.ts
@@ -11,52 +11,45 @@
import { lazyRouteComponent } from '@tanstack/vue-router'
import { Route as rootRouteImport } from './routes/__root'
-import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
-import { Route as SfcComponentRouteImport } from './routes/sfcComponent'
-import { Route as RemountDepsRouteImport } from './routes/remountDeps'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
-import { Route as EditingBRouteImport } from './routes/editing-b'
-import { Route as EditingARouteImport } from './routes/editing-a'
-import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as EditingARouteImport } from './routes/editing-a'
+import { Route as EditingBRouteImport } from './routes/editing-b'
+import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RemountDepsRouteImport } from './routes/remountDeps'
+import { Route as SfcComponentRouteImport } from './routes/sfcComponent'
+import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
+import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
+import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
+import { Route as groupInsideRouteImport } from './routes/(group)/inside'
+import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
-import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
-import { Route as groupInsideRouteImport } from './routes/(group)/inside'
-import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
-import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
-import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
-import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
-import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
-import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout'
+import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
-const Char45824Char54620Char48124Char44397Route =
- Char45824Char54620Char48124Char44397RouteImport.update({
- id: '/대한민국',
- path: '/대한민국',
- getParentRoute: () => rootRouteImport,
- } as any)
-const SfcComponentRoute = SfcComponentRouteImport.update({
- id: '/sfcComponent',
- path: '/sfcComponent',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
-} as any).update({
- component: lazyRouteComponent(
- () => import('./routes/sfcComponent.component.vue'),
- 'default',
- ),
-})
-const RemountDepsRoute = RemountDepsRouteImport.update({
- id: '/remountDeps',
- path: '/remountDeps',
+} as any)
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const EditingARoute = EditingARouteImport.update({
+ id: '/editing-a',
+ path: '/editing-a',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const EditingBRoute = EditingBRouteImport.update({
+ id: '/editing-b',
+ path: '/editing-b',
getParentRoute: () => rootRouteImport,
} as any)
const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
@@ -64,25 +57,56 @@ const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
path: '/notRemountDeps',
getParentRoute: () => rootRouteImport,
} as any)
-const EditingBRoute = EditingBRouteImport.update({
- id: '/editing-b',
- path: '/editing-b',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
-const EditingARoute = EditingARouteImport.update({
- id: '/editing-a',
- path: '/editing-a',
+const RemountDepsRoute = RemountDepsRouteImport.update({
+ id: '/remountDeps',
+ path: '/remountDeps',
getParentRoute: () => rootRouteImport,
} as any)
-const LayoutRoute = LayoutRouteImport.update({
- id: '/_layout',
+const SfcComponentRoute = SfcComponentRouteImport.update({
+ id: '/sfcComponent',
+ path: '/sfcComponent',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/sfcComponent.component.vue'),
+ 'default',
+ ),
+})
+const Char45824Char54620Char48124Char44397Route =
+ Char45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const anotherGroupOnlyrouteinsideRoute =
+ anotherGroupOnlyrouteinsideRouteImport.update({
+ id: '/(another-group)/onlyrouteinside',
+ path: '/onlyrouteinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const groupLayoutRoute = groupLayoutRouteImport.update({
+ id: '/(group)/_layout',
getParentRoute: () => rootRouteImport,
} as any)
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const groupInsideRoute = groupInsideRouteImport.update({
+ id: '/(group)/inside',
+ path: '/inside',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const groupLazyinsideRoute = groupLazyinsideRouteImport.update({
+ id: '/(group)/lazyinside',
+ path: '/lazyinside',
getParentRoute: () => rootRouteImport,
} as any)
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
+} as any)
const PostsIndexRoute = PostsIndexRouteImport.update({
id: '/',
path: '/',
@@ -93,55 +117,31 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({
path: '/$postId',
getParentRoute: () => PostsRoute,
} as any)
-const LayoutLayout2Route = LayoutLayout2RouteImport.update({
- id: '/_layout-2',
- getParentRoute: () => LayoutRoute,
-} as any)
-const groupLazyinsideRoute = groupLazyinsideRouteImport.update({
- id: '/(group)/lazyinside',
- path: '/lazyinside',
- getParentRoute: () => rootRouteImport,
-} as any)
-const groupInsideRoute = groupInsideRouteImport.update({
- id: '/(group)/inside',
- path: '/inside',
- getParentRoute: () => rootRouteImport,
+const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport.update({
+ id: '/insidelayout',
+ path: '/insidelayout',
+ getParentRoute: () => groupLayoutRoute,
} as any)
-const groupLayoutRoute = groupLayoutRouteImport.update({
- id: '/(group)/_layout',
+const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport.update({
+ id: '/(group)/subfolder/inside',
+ path: '/subfolder/inside',
getParentRoute: () => rootRouteImport,
} as any)
-const anotherGroupOnlyrouteinsideRoute =
- anotherGroupOnlyrouteinsideRouteImport.update({
- id: '/(another-group)/onlyrouteinside',
- path: '/onlyrouteinside',
- getParentRoute: () => rootRouteImport,
- } as any)
-const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
- id: '/posts_/$postId/edit',
- path: '/posts/$postId/edit',
- getParentRoute: () => rootRouteImport,
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
} as any)
const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
id: '/layout-b',
path: '/layout-b',
getParentRoute: () => LayoutLayout2Route,
} as any)
-const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
- id: '/layout-a',
- path: '/layout-a',
- getParentRoute: () => LayoutLayout2Route,
-} as any)
-const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport.update({
- id: '/(group)/subfolder/inside',
- path: '/subfolder/inside',
+const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
+ id: '/posts_/$postId/edit',
+ path: '/posts/$postId/edit',
getParentRoute: () => rootRouteImport,
} as any)
-const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport.update({
- id: '/insidelayout',
- path: '/insidelayout',
- getParentRoute: () => groupLayoutRoute,
-} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -291,32 +291,32 @@ export interface RootRouteChildren {
declare module '@tanstack/vue-router' {
interface FileRoutesByPath {
- '/대한민국': {
- id: '/대한민국'
- path: '/대한민국'
- fullPath: '/대한민국'
- preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/sfcComponent': {
- id: '/sfcComponent'
- path: '/sfcComponent'
- fullPath: '/sfcComponent'
- preLoaderRoute: typeof SfcComponentRouteImport
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/remountDeps': {
- id: '/remountDeps'
- path: '/remountDeps'
- fullPath: '/remountDeps'
- preLoaderRoute: typeof RemountDepsRouteImport
+ '/editing-a': {
+ id: '/editing-a'
+ path: '/editing-a'
+ fullPath: '/editing-a'
+ preLoaderRoute: typeof EditingARouteImport
parentRoute: typeof rootRouteImport
}
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/editing-b': {
+ id: '/editing-b'
+ path: '/editing-b'
+ fullPath: '/editing-b'
+ preLoaderRoute: typeof EditingBRouteImport
parentRoute: typeof rootRouteImport
}
'/notRemountDeps': {
@@ -326,34 +326,69 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof NotRemountDepsRouteImport
parentRoute: typeof rootRouteImport
}
- '/editing-b': {
- id: '/editing-b'
- path: '/editing-b'
- fullPath: '/editing-b'
- preLoaderRoute: typeof EditingBRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/editing-a': {
- id: '/editing-a'
- path: '/editing-a'
- fullPath: '/editing-a'
- preLoaderRoute: typeof EditingARouteImport
+ '/remountDeps': {
+ id: '/remountDeps'
+ path: '/remountDeps'
+ fullPath: '/remountDeps'
+ preLoaderRoute: typeof RemountDepsRouteImport
parentRoute: typeof rootRouteImport
}
- '/_layout': {
- id: '/_layout'
+ '/sfcComponent': {
+ id: '/sfcComponent'
+ path: '/sfcComponent'
+ fullPath: '/sfcComponent'
+ preLoaderRoute: typeof SfcComponentRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(another-group)/onlyrouteinside': {
+ id: '/(another-group)/onlyrouteinside'
+ path: '/onlyrouteinside'
+ fullPath: '/onlyrouteinside'
+ preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout': {
+ id: '/(group)/_layout'
path: ''
- fullPath: '/'
- preLoaderRoute: typeof LayoutRouteImport
+ fullPath: ''
+ preLoaderRoute: typeof groupLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/(group)/inside': {
+ id: '/(group)/inside'
+ path: '/inside'
+ fullPath: '/inside'
+ preLoaderRoute: typeof groupInsideRouteImport
parentRoute: typeof rootRouteImport
}
+ '/(group)/lazyinside': {
+ id: '/(group)/lazyinside'
+ path: '/lazyinside'
+ fullPath: '/lazyinside'
+ preLoaderRoute: typeof groupLazyinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRoute
+ }
'/posts/': {
id: '/posts/'
path: '/'
@@ -368,47 +403,26 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/_layout/_layout-2': {
- id: '/_layout/_layout-2'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof LayoutLayout2RouteImport
- parentRoute: typeof LayoutRoute
- }
- '/(group)/lazyinside': {
- id: '/(group)/lazyinside'
- path: '/lazyinside'
- fullPath: '/lazyinside'
- preLoaderRoute: typeof groupLazyinsideRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/(group)/inside': {
- id: '/(group)/inside'
- path: '/inside'
- fullPath: '/inside'
- preLoaderRoute: typeof groupInsideRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/(group)/_layout': {
- id: '/(group)/_layout'
- path: ''
- fullPath: ''
- preLoaderRoute: typeof groupLayoutRouteImport
- parentRoute: typeof rootRouteImport
+ '/(group)/_layout/insidelayout': {
+ id: '/(group)/_layout/insidelayout'
+ path: '/insidelayout'
+ fullPath: '/insidelayout'
+ preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
+ parentRoute: typeof groupLayoutRoute
}
- '/(another-group)/onlyrouteinside': {
- id: '/(another-group)/onlyrouteinside'
- path: '/onlyrouteinside'
- fullPath: '/onlyrouteinside'
- preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ '/(group)/subfolder/inside': {
+ id: '/(group)/subfolder/inside'
+ path: '/subfolder/inside'
+ fullPath: '/subfolder/inside'
+ preLoaderRoute: typeof groupSubfolderInsideRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts_/$postId/edit': {
- id: '/posts_/$postId/edit'
- path: '/posts/$postId/edit'
- fullPath: '/posts/$postId/edit'
- preLoaderRoute: typeof PostsPostIdEditRouteImport
- parentRoute: typeof rootRouteImport
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
}
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
@@ -417,27 +431,13 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
parentRoute: typeof LayoutLayout2Route
}
- '/_layout/_layout-2/layout-a': {
- id: '/_layout/_layout-2/layout-a'
- path: '/layout-a'
- fullPath: '/layout-a'
- preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
- parentRoute: typeof LayoutLayout2Route
- }
- '/(group)/subfolder/inside': {
- id: '/(group)/subfolder/inside'
- path: '/subfolder/inside'
- fullPath: '/subfolder/inside'
- preLoaderRoute: typeof groupSubfolderInsideRouteImport
+ '/posts_/$postId/edit': {
+ id: '/posts_/$postId/edit'
+ path: '/posts/$postId/edit'
+ fullPath: '/posts/$postId/edit'
+ preLoaderRoute: typeof PostsPostIdEditRouteImport
parentRoute: typeof rootRouteImport
}
- '/(group)/_layout/insidelayout': {
- id: '/(group)/_layout/insidelayout'
- path: '/insidelayout'
- fullPath: '/insidelayout'
- preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
- parentRoute: typeof groupLayoutRoute
- }
}
}
diff --git a/examples/vue/basic-file-based-sfc/package.json b/examples/vue/basic-file-based-sfc/package.json
index 91581cc30d..f2a41544cb 100644
--- a/examples/vue/basic-file-based-sfc/package.json
+++ b/examples/vue/basic-file-based-sfc/package.json
@@ -10,9 +10,9 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.2.2",
- "@tanstack/router-plugin": "^1.168.19",
- "@tanstack/vue-router": "^1.170.16",
- "@tanstack/vue-router-devtools": "^1.167.0",
+ "@tanstack/router-plugin": "^1.168.24",
+ "@tanstack/vue-router": "^1.170.18",
+ "@tanstack/vue-router-devtools": "^1.167.1",
"redaxios": "^0.5.1",
"tailwindcss": "^4.2.2",
"vue": "^3.5.16",
@@ -23,6 +23,6 @@
"@vitejs/plugin-vue-jsx": "^5.1.5",
"typescript": "~5.8.3",
"vite": "^8.0.14",
- "vue-tsc": "^3.1.5"
+ "vue-tsc": "^3.3.8"
}
}
diff --git a/examples/vue/basic-file-based-sfc/src/routeTree.gen.ts b/examples/vue/basic-file-based-sfc/src/routeTree.gen.ts
index fa00eb1ce0..09d62b0706 100644
--- a/examples/vue/basic-file-based-sfc/src/routeTree.gen.ts
+++ b/examples/vue/basic-file-based-sfc/src/routeTree.gen.ts
@@ -11,65 +11,53 @@
import { lazyRouteComponent } from '@tanstack/vue-router'
import { Route as rootRouteImport } from './routes/__root'
-import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
-import { Route as RemountDepsRouteImport } from './routes/remountDeps'
-import { Route as PostsRouteImport } from './routes/posts'
-import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
-import { Route as EditingBRouteImport } from './routes/editing-b'
-import { Route as EditingARouteImport } from './routes/editing-a'
-import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as EditingARouteImport } from './routes/editing-a'
+import { Route as EditingBRouteImport } from './routes/editing-b'
+import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
+import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RemountDepsRouteImport } from './routes/remountDeps'
+import { Route as Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
+import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
+import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
+import { Route as groupInsideRouteImport } from './routes/(group)/inside'
+import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
+import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
-import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
-import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside'
-import { Route as groupInsideRouteImport } from './routes/(group)/inside'
-import { Route as groupLayoutRouteImport } from './routes/(group)/_layout'
-import { Route as anotherGroupOnlyrouteinsideRouteImport } from './routes/(another-group)/onlyrouteinside'
-import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
-import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
-import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
-import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
import { Route as groupLayoutInsidelayoutRouteImport } from './routes/(group)/_layout.insidelayout'
+import { Route as groupSubfolderInsideRouteImport } from './routes/(group)/subfolder/inside'
+import { Route as LayoutLayout2LayoutARouteImport } from './routes/_layout/_layout-2/layout-a'
+import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as PostsPostIdEditRouteImport } from './routes/posts_.$postId.edit'
-const Char45824Char54620Char48124Char44397Route =
- Char45824Char54620Char48124Char44397RouteImport.update({
- id: '/대한민국',
- path: '/대한민국',
- getParentRoute: () => rootRouteImport,
- } as any).update({
- component: lazyRouteComponent(
- () => import('./routes/대한민국.component.vue'),
- 'default',
- ),
- })
-const RemountDepsRoute = RemountDepsRouteImport.update({
- id: '/remountDeps',
- path: '/remountDeps',
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
getParentRoute: () => rootRouteImport,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/remountDeps.component.vue'),
+ () => import('./routes/index.component.vue'),
'default',
),
})
-const PostsRoute = PostsRouteImport.update({
- id: '/posts',
- path: '/posts',
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/posts.component.vue'),
+ () => import('./routes/_layout.component.vue'),
'default',
),
})
-const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
- id: '/notRemountDeps',
- path: '/notRemountDeps',
+const EditingARoute = EditingARouteImport.update({
+ id: '/editing-a',
+ path: '/editing-a',
getParentRoute: () => rootRouteImport,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/notRemountDeps.component.vue'),
+ () => import('./routes/editing-a.component.vue'),
'default',
),
})
@@ -83,89 +71,56 @@ const EditingBRoute = EditingBRouteImport.update({
'default',
),
})
-const EditingARoute = EditingARouteImport.update({
- id: '/editing-a',
- path: '/editing-a',
+const NotRemountDepsRoute = NotRemountDepsRouteImport.update({
+ id: '/notRemountDeps',
+ path: '/notRemountDeps',
getParentRoute: () => rootRouteImport,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/editing-a.component.vue'),
+ () => import('./routes/notRemountDeps.component.vue'),
'default',
),
})
-const LayoutRoute = LayoutRouteImport.update({
- id: '/_layout',
+const PostsRoute = PostsRouteImport.update({
+ id: '/posts',
+ path: '/posts',
getParentRoute: () => rootRouteImport,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/_layout.component.vue'),
+ () => import('./routes/posts.component.vue'),
'default',
),
})
-const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+const RemountDepsRoute = RemountDepsRouteImport.update({
+ id: '/remountDeps',
+ path: '/remountDeps',
getParentRoute: () => rootRouteImport,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/index.component.vue'),
- 'default',
- ),
-})
-const PostsIndexRoute = PostsIndexRouteImport.update({
- id: '/',
- path: '/',
- getParentRoute: () => PostsRoute,
-} as any).update({
- component: lazyRouteComponent(
- () => import('./routes/posts.index.component.vue'),
- 'default',
- ),
-})
-const PostsPostIdRoute = PostsPostIdRouteImport.update({
- id: '/$postId',
- path: '/$postId',
- getParentRoute: () => PostsRoute,
-} as any).update({
- component: lazyRouteComponent(
- () => import('./routes/posts.$postId.component.vue'),
- 'default',
- ),
- errorComponent: lazyRouteComponent(
- () => import('./routes/posts.$postId.errorComponent.vue'),
- 'default',
- ),
-})
-const LayoutLayout2Route = LayoutLayout2RouteImport.update({
- id: '/_layout-2',
- getParentRoute: () => LayoutRoute,
-} as any).update({
- component: lazyRouteComponent(
- () => import('./routes/_layout/_layout-2.component.vue'),
+ () => import('./routes/remountDeps.component.vue'),
'default',
),
})
-const groupLazyinsideRoute = groupLazyinsideRouteImport
- .update({
- id: '/(group)/lazyinside',
- path: '/lazyinside',
+const Char45824Char54620Char48124Char44397Route =
+ Char45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
getParentRoute: () => rootRouteImport,
- } as any)
- .update({
+ } as any).update({
component: lazyRouteComponent(
- () => import('./routes/(group)/lazyinside.component.vue'),
+ () => import('./routes/대한민국.component.vue'),
'default',
),
})
-const groupInsideRoute = groupInsideRouteImport
+const anotherGroupOnlyrouteinsideRoute = anotherGroupOnlyrouteinsideRouteImport
.update({
- id: '/(group)/inside',
- path: '/inside',
+ id: '/(another-group)/onlyrouteinside',
+ path: '/onlyrouteinside',
getParentRoute: () => rootRouteImport,
} as any)
.update({
component: lazyRouteComponent(
- () => import('./routes/(group)/inside.component.vue'),
+ () => import('./routes/(another-group)/onlyrouteinside.component.vue'),
'default',
),
})
@@ -180,72 +135,117 @@ const groupLayoutRoute = groupLayoutRouteImport
'default',
),
})
-const anotherGroupOnlyrouteinsideRoute = anotherGroupOnlyrouteinsideRouteImport
+const groupInsideRoute = groupInsideRouteImport
.update({
- id: '/(another-group)/onlyrouteinside',
- path: '/onlyrouteinside',
+ id: '/(group)/inside',
+ path: '/inside',
getParentRoute: () => rootRouteImport,
} as any)
.update({
component: lazyRouteComponent(
- () => import('./routes/(another-group)/onlyrouteinside.component.vue'),
+ () => import('./routes/(group)/inside.component.vue'),
'default',
),
})
-const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
- id: '/posts_/$postId/edit',
- path: '/posts/$postId/edit',
- getParentRoute: () => rootRouteImport,
+const groupLazyinsideRoute = groupLazyinsideRouteImport
+ .update({
+ id: '/(group)/lazyinside',
+ path: '/lazyinside',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+ .update({
+ component: lazyRouteComponent(
+ () => import('./routes/(group)/lazyinside.component.vue'),
+ 'default',
+ ),
+ })
+const LayoutLayout2Route = LayoutLayout2RouteImport.update({
+ id: '/_layout-2',
+ getParentRoute: () => LayoutRoute,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/posts_.$postId.edit.component.vue'),
+ () => import('./routes/_layout/_layout-2.component.vue'),
'default',
),
})
-const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
- id: '/layout-b',
- path: '/layout-b',
- getParentRoute: () => LayoutLayout2Route,
+const PostsIndexRoute = PostsIndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => PostsRoute,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/_layout/_layout-2/layout-b.component.vue'),
+ () => import('./routes/posts.index.component.vue'),
'default',
),
})
-const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
- id: '/layout-a',
- path: '/layout-a',
- getParentRoute: () => LayoutLayout2Route,
+const PostsPostIdRoute = PostsPostIdRouteImport.update({
+ id: '/$postId',
+ path: '/$postId',
+ getParentRoute: () => PostsRoute,
} as any).update({
component: lazyRouteComponent(
- () => import('./routes/_layout/_layout-2/layout-a.component.vue'),
+ () => import('./routes/posts.$postId.component.vue'),
+ 'default',
+ ),
+ errorComponent: lazyRouteComponent(
+ () => import('./routes/posts.$postId.errorComponent.vue'),
'default',
),
})
-const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport
+const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport
.update({
- id: '/(group)/subfolder/inside',
- path: '/subfolder/inside',
- getParentRoute: () => rootRouteImport,
+ id: '/insidelayout',
+ path: '/insidelayout',
+ getParentRoute: () => groupLayoutRoute,
} as any)
.update({
component: lazyRouteComponent(
- () => import('./routes/(group)/subfolder/inside.component.vue'),
+ () => import('./routes/(group)/_layout.insidelayout.component.vue'),
'default',
),
})
-const groupLayoutInsidelayoutRoute = groupLayoutInsidelayoutRouteImport
+const groupSubfolderInsideRoute = groupSubfolderInsideRouteImport
.update({
- id: '/insidelayout',
- path: '/insidelayout',
- getParentRoute: () => groupLayoutRoute,
+ id: '/(group)/subfolder/inside',
+ path: '/subfolder/inside',
+ getParentRoute: () => rootRouteImport,
} as any)
.update({
component: lazyRouteComponent(
- () => import('./routes/(group)/_layout.insidelayout.component.vue'),
+ () => import('./routes/(group)/subfolder/inside.component.vue'),
'default',
),
})
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
+ id: '/layout-a',
+ path: '/layout-a',
+ getParentRoute: () => LayoutLayout2Route,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2/layout-a.component.vue'),
+ 'default',
+ ),
+})
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBRouteImport.update({
+ id: '/layout-b',
+ path: '/layout-b',
+ getParentRoute: () => LayoutLayout2Route,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/_layout/_layout-2/layout-b.component.vue'),
+ 'default',
+ ),
+})
+const PostsPostIdEditRoute = PostsPostIdEditRouteImport.update({
+ id: '/posts_/$postId/edit',
+ path: '/posts/$postId/edit',
+ getParentRoute: () => rootRouteImport,
+} as any).update({
+ component: lazyRouteComponent(
+ () => import('./routes/posts_.$postId.edit.component.vue'),
+ 'default',
+ ),
+})
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
@@ -388,25 +388,32 @@ export interface RootRouteChildren {
declare module '@tanstack/vue-router' {
interface FileRoutesByPath {
- '/대한민국': {
- id: '/대한민국'
- path: '/대한민국'
- fullPath: '/대한민국'
- preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/remountDeps': {
- id: '/remountDeps'
- path: '/remountDeps'
- fullPath: '/remountDeps'
- preLoaderRoute: typeof RemountDepsRouteImport
+ '/_layout': {
+ id: '/_layout'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts': {
- id: '/posts'
- path: '/posts'
- fullPath: '/posts'
- preLoaderRoute: typeof PostsRouteImport
+ '/editing-a': {
+ id: '/editing-a'
+ path: '/editing-a'
+ fullPath: '/editing-a'
+ preLoaderRoute: typeof EditingARouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/editing-b': {
+ id: '/editing-b'
+ path: '/editing-b'
+ fullPath: '/editing-b'
+ preLoaderRoute: typeof EditingBRouteImport
parentRoute: typeof rootRouteImport
}
'/notRemountDeps': {
@@ -416,34 +423,62 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof NotRemountDepsRouteImport
parentRoute: typeof rootRouteImport
}
- '/editing-b': {
- id: '/editing-b'
- path: '/editing-b'
- fullPath: '/editing-b'
- preLoaderRoute: typeof EditingBRouteImport
+ '/posts': {
+ id: '/posts'
+ path: '/posts'
+ fullPath: '/posts'
+ preLoaderRoute: typeof PostsRouteImport
parentRoute: typeof rootRouteImport
}
- '/editing-a': {
- id: '/editing-a'
- path: '/editing-a'
- fullPath: '/editing-a'
- preLoaderRoute: typeof EditingARouteImport
+ '/remountDeps': {
+ id: '/remountDeps'
+ path: '/remountDeps'
+ fullPath: '/remountDeps'
+ preLoaderRoute: typeof RemountDepsRouteImport
parentRoute: typeof rootRouteImport
}
- '/_layout': {
- id: '/_layout'
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(another-group)/onlyrouteinside': {
+ id: '/(another-group)/onlyrouteinside'
+ path: '/onlyrouteinside'
+ fullPath: '/onlyrouteinside'
+ preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/(group)/_layout': {
+ id: '/(group)/_layout'
path: ''
- fullPath: '/'
- preLoaderRoute: typeof LayoutRouteImport
+ fullPath: ''
+ preLoaderRoute: typeof groupLayoutRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
+ '/(group)/inside': {
+ id: '/(group)/inside'
+ path: '/inside'
+ fullPath: '/inside'
+ preLoaderRoute: typeof groupInsideRouteImport
parentRoute: typeof rootRouteImport
}
+ '/(group)/lazyinside': {
+ id: '/(group)/lazyinside'
+ path: '/lazyinside'
+ fullPath: '/lazyinside'
+ preLoaderRoute: typeof groupLazyinsideRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout/_layout-2': {
+ id: '/_layout/_layout-2'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof LayoutLayout2RouteImport
+ parentRoute: typeof LayoutRoute
+ }
'/posts/': {
id: '/posts/'
path: '/'
@@ -458,47 +493,26 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof PostsPostIdRouteImport
parentRoute: typeof PostsRoute
}
- '/_layout/_layout-2': {
- id: '/_layout/_layout-2'
- path: ''
- fullPath: '/'
- preLoaderRoute: typeof LayoutLayout2RouteImport
- parentRoute: typeof LayoutRoute
- }
- '/(group)/lazyinside': {
- id: '/(group)/lazyinside'
- path: '/lazyinside'
- fullPath: '/lazyinside'
- preLoaderRoute: typeof groupLazyinsideRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/(group)/inside': {
- id: '/(group)/inside'
- path: '/inside'
- fullPath: '/inside'
- preLoaderRoute: typeof groupInsideRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/(group)/_layout': {
- id: '/(group)/_layout'
- path: ''
- fullPath: ''
- preLoaderRoute: typeof groupLayoutRouteImport
- parentRoute: typeof rootRouteImport
+ '/(group)/_layout/insidelayout': {
+ id: '/(group)/_layout/insidelayout'
+ path: '/insidelayout'
+ fullPath: '/insidelayout'
+ preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
+ parentRoute: typeof groupLayoutRoute
}
- '/(another-group)/onlyrouteinside': {
- id: '/(another-group)/onlyrouteinside'
- path: '/onlyrouteinside'
- fullPath: '/onlyrouteinside'
- preLoaderRoute: typeof anotherGroupOnlyrouteinsideRouteImport
+ '/(group)/subfolder/inside': {
+ id: '/(group)/subfolder/inside'
+ path: '/subfolder/inside'
+ fullPath: '/subfolder/inside'
+ preLoaderRoute: typeof groupSubfolderInsideRouteImport
parentRoute: typeof rootRouteImport
}
- '/posts_/$postId/edit': {
- id: '/posts_/$postId/edit'
- path: '/posts/$postId/edit'
- fullPath: '/posts/$postId/edit'
- preLoaderRoute: typeof PostsPostIdEditRouteImport
- parentRoute: typeof rootRouteImport
+ '/_layout/_layout-2/layout-a': {
+ id: '/_layout/_layout-2/layout-a'
+ path: '/layout-a'
+ fullPath: '/layout-a'
+ preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
+ parentRoute: typeof LayoutLayout2Route
}
'/_layout/_layout-2/layout-b': {
id: '/_layout/_layout-2/layout-b'
@@ -507,27 +521,13 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof LayoutLayout2LayoutBRouteImport
parentRoute: typeof LayoutLayout2Route
}
- '/_layout/_layout-2/layout-a': {
- id: '/_layout/_layout-2/layout-a'
- path: '/layout-a'
- fullPath: '/layout-a'
- preLoaderRoute: typeof LayoutLayout2LayoutARouteImport
- parentRoute: typeof LayoutLayout2Route
- }
- '/(group)/subfolder/inside': {
- id: '/(group)/subfolder/inside'
- path: '/subfolder/inside'
- fullPath: '/subfolder/inside'
- preLoaderRoute: typeof groupSubfolderInsideRouteImport
+ '/posts_/$postId/edit': {
+ id: '/posts_/$postId/edit'
+ path: '/posts/$postId/edit'
+ fullPath: '/posts/$postId/edit'
+ preLoaderRoute: typeof PostsPostIdEditRouteImport
parentRoute: typeof rootRouteImport
}
- '/(group)/_layout/insidelayout': {
- id: '/(group)/_layout/insidelayout'
- path: '/insidelayout'
- fullPath: '/insidelayout'
- preLoaderRoute: typeof groupLayoutInsidelayoutRouteImport
- parentRoute: typeof groupLayoutRoute
- }
}
}
diff --git a/examples/vue/basic/package.json b/examples/vue/basic/package.json
index 046be6614d..16c55a19b5 100644
--- a/examples/vue/basic/package.json
+++ b/examples/vue/basic/package.json
@@ -9,16 +9,17 @@
"start": "vite"
},
"dependencies": {
- "@tanstack/vue-router": "^1.170.16",
- "@tanstack/vue-router-devtools": "^1.167.0",
+ "@tanstack/vue-router": "^1.170.18",
+ "@tanstack/vue-router-devtools": "^1.167.1",
"redaxios": "^0.5.1",
"vue": "^3.5.13",
"tailwindcss": "4.1.18"
},
"devDependencies": {
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.0.14",
- "vue-tsc": "^3.1.5",
+ "vue-tsc": "^3.3.8",
"@vitejs/plugin-vue": "^6.0.5",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@tailwindcss/vite": "^4.2.2"
diff --git a/media/header_router.png b/media/header_router.png
deleted file mode 100644
index 5f2d58134d..0000000000
Binary files a/media/header_router.png and /dev/null differ
diff --git a/media/header_start.png b/media/header_start.png
deleted file mode 100644
index 8622986ee1..0000000000
Binary files a/media/header_start.png and /dev/null differ
diff --git a/nx.json b/nx.json
index 08e7563642..764112a2f6 100644
--- a/nx.json
+++ b/nx.json
@@ -9,10 +9,18 @@
},
"namedInputs": {
"sharedGlobals": [
+ "{workspaceRoot}/.npmrc",
+ "{workspaceRoot}/.nx/workflows/sandboxing-config.yaml",
"{workspaceRoot}/.nvmrc",
"{workspaceRoot}/package.json",
"{workspaceRoot}/tsconfig.json"
],
+ "dependentTaskOutputs": [
+ {
+ "dependentTasksOutputFiles": "**/*",
+ "transitive": true
+ }
+ ],
"default": [
"sharedGlobals",
"{projectRoot}/**/*",
@@ -22,7 +30,8 @@
"default",
"!{projectRoot}/tests/**/*",
"!{projectRoot}/eslint.config.js"
- ]
+ ],
+ "buildProduction": ["sharedGlobals", "{projectRoot}/**/*"]
},
"targetDefaults": {
"test:docs": {
@@ -31,19 +40,34 @@
},
"test:eslint": {
"cache": true,
- "dependsOn": ["^build"],
- "inputs": ["default", "^production", "{workspaceRoot}/eslint.config.js"]
+ "dependsOn": ["^build", "build"],
+ "inputs": [
+ "default",
+ "^production",
+ "{workspaceRoot}/eslint.config.js",
+ "dependentTaskOutputs"
+ ]
},
"test:unit": {
"cache": true,
"dependsOn": ["^build"],
- "inputs": ["default", "^production"],
+ "inputs": [
+ "default",
+ "^production",
+ "{workspaceRoot}/eslint.config.js",
+ "dependentTaskOutputs"
+ ],
"outputs": ["{projectRoot}/coverage"]
},
"test:e2e": {
"cache": true,
"dependsOn": ["^build", "test:e2e--*"],
- "inputs": ["default", "^production"]
+ "inputs": ["default", "^production", "dependentTaskOutputs"],
+ "outputs": [
+ "{projectRoot}/dist",
+ "{projectRoot}/port-*.txt",
+ "{projectRoot}/test-results"
+ ]
},
"test:e2e:nitro": {
"cache": true,
@@ -62,24 +86,40 @@
},
"test:types": {
"cache": true,
- "dependsOn": ["^build"],
- "inputs": ["default", "^production"]
+ "dependsOn": ["^build", "build"],
+ "inputs": [
+ "default",
+ "^production",
+ "{workspaceRoot}/eslint.config.js",
+ "dependentTaskOutputs"
+ ]
},
"test:types:ssr": {
"cache": true,
- "dependsOn": ["^build"],
- "inputs": ["default", "^production"]
+ "dependsOn": ["^build", "build"],
+ "inputs": [
+ "default",
+ "^production",
+ "{workspaceRoot}/benchmarks/ssr/bench-utils.ts",
+ "dependentTaskOutputs"
+ ]
},
"build": {
"cache": true,
"dependsOn": ["^build"],
- "inputs": ["production", "^production"],
- "outputs": ["{projectRoot}/build", "{projectRoot}/dist"]
+ "inputs": ["buildProduction", "^buildProduction", "dependentTaskOutputs"],
+ "outputs": [
+ "{projectRoot}/.netlify",
+ "{projectRoot}/.wrangler",
+ "{projectRoot}/.output",
+ "{projectRoot}/build",
+ "{projectRoot}/dist"
+ ]
},
"test:build": {
"cache": true,
"dependsOn": ["build"],
- "inputs": ["production"]
+ "inputs": ["buildProduction", "dependentTaskOutputs"]
}
},
"plugins": [
diff --git a/package.json b/package.json
index cdc2bf2bc8..98682bac38 100644
--- a/package.json
+++ b/package.json
@@ -93,13 +93,13 @@
"redaxios": "^0.5.1",
"rimraf": "^6.1.2",
"tinyglobby": "^0.2.15",
- "typescript": "^6.0.2",
- "typescript55": "npm:typescript@5.5",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
+ "vite": "catalog:",
+ "vitest": "^4.1.4",
+ "@typescript/native": "npm:typescript@^7.0.2",
"typescript56": "npm:typescript@5.6",
"typescript57": "npm:typescript@5.7",
"typescript58": "npm:typescript@5.8",
- "typescript59": "npm:typescript@5.9",
- "vite": "catalog:",
- "vitest": "^4.1.4"
+ "typescript59": "npm:typescript@5.9"
}
}
diff --git a/packages/arktype-adapter/package.json b/packages/arktype-adapter/package.json
index f7840ba4f8..34bd520f9b 100644
--- a/packages/arktype-adapter/package.json
+++ b/packages/arktype-adapter/package.json
@@ -27,12 +27,12 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:unit": "vitest",
"test:unit:dev": "pnpm run test:unit --watch --typecheck",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
diff --git a/packages/eslint-plugin-router/package.json b/packages/eslint-plugin-router/package.json
index b99ee55e13..25e742f8f7 100644
--- a/packages/eslint-plugin-router/package.json
+++ b/packages/eslint-plugin-router/package.json
@@ -18,12 +18,12 @@
"clean": "rimraf ./dist ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:unit": "vitest",
"test:unit:dev": "pnpm run test:unit --watch --typecheck",
"test:build": "publint --strict && attw --pack .",
diff --git a/packages/eslint-plugin-start/package.json b/packages/eslint-plugin-start/package.json
index c4867c58d0..df48592327 100644
--- a/packages/eslint-plugin-start/package.json
+++ b/packages/eslint-plugin-start/package.json
@@ -18,12 +18,12 @@
"clean": "rimraf ./dist ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:unit": "vitest",
"test:unit:dev": "pnpm run test:unit --watch --typecheck",
"test:build": "publint --strict && attw --pack .",
@@ -61,7 +61,7 @@
"typescript": "^5.8.2"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.7.0"
}
}
diff --git a/packages/history/package.json b/packages/history/package.json
index 790acabc3c..52e4641a01 100644
--- a/packages/history/package.json
+++ b/packages/history/package.json
@@ -22,12 +22,12 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"test:unit": "vitest",
"test:unit:dev": "pnpm run test:unit --watch",
diff --git a/packages/nitro-v2-vite-plugin/package.json b/packages/nitro-v2-vite-plugin/package.json
index b3021ea3c6..68966c5011 100644
--- a/packages/nitro-v2-vite-plugin/package.json
+++ b/packages/nitro-v2-vite-plugin/package.json
@@ -29,12 +29,12 @@
"test:unit": "echo 'No unit tests are needed here since we do them in @tanstack/router-plugin!'",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"build": "vite build"
},
diff --git a/packages/react-router-devtools/CHANGELOG.md b/packages/react-router-devtools/CHANGELOG.md
index 40e065c673..7634f3d19e 100644
--- a/packages/react-router-devtools/CHANGELOG.md
+++ b/packages/react-router-devtools/CHANGELOG.md
@@ -1,5 +1,14 @@
# @tanstack/react-router-devtools
+## 1.167.1
+
+### Patch Changes
+
+- Updated dependencies [[`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd)]:
+ - @tanstack/router-core@1.171.16
+ - @tanstack/react-router@1.170.19
+ - @tanstack/router-devtools-core@1.168.1
+
## 1.167.0
### Minor Changes
diff --git a/packages/react-router-devtools/README.md b/packages/react-router-devtools/README.md
index 7be76f46c2..0079d7c065 100644
--- a/packages/react-router-devtools/README.md
+++ b/packages/react-router-devtools/README.md
@@ -1,5 +1,22 @@
+
+
+
+
+
+
+
# TanStack React Router Devtools
See https://tanstack.com/router/latest/docs/framework/react/devtools
diff --git a/packages/react-router-devtools/package.json b/packages/react-router-devtools/package.json
index 3cb40e24b0..262a9294fb 100644
--- a/packages/react-router-devtools/package.json
+++ b/packages/react-router-devtools/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-router-devtools",
- "version": "1.167.0",
+ "version": "1.167.1",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -27,12 +27,12 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"build": "vite build"
},
diff --git a/packages/react-router-ssr-query/README.md b/packages/react-router-ssr-query/README.md
index 0a19fd3b81..c3a60d1008 100644
--- a/packages/react-router-ssr-query/README.md
+++ b/packages/react-router-ssr-query/README.md
@@ -2,7 +2,21 @@
# TanStack React Router
-
+
+
+
+
+
🤖 Type-safe router w/ built-in caching & URL state management for React!
diff --git a/packages/react-router-ssr-query/package.json b/packages/react-router-ssr-query/package.json
index 82d9917747..2967d695b0 100644
--- a/packages/react-router-ssr-query/package.json
+++ b/packages/react-router-ssr-query/package.json
@@ -27,12 +27,12 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:unit": "exit 0; vitest",
"test:unit:dev": "pnpm run test:unit --watch",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md
index dc138ae4ed..49e069817b 100644
--- a/packages/react-router/CHANGELOG.md
+++ b/packages/react-router/CHANGELOG.md
@@ -1,5 +1,45 @@
# @tanstack/react-router
+## 1.170.19
+
+### Patch Changes
+
+- [#7805](https://github.com/TanStack/router/pull/7805) [`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd) - Rewrite match loading around a lane-based scheduler that tracks each navigation, preload, and background reload as an ordered unit of work. This fixes pending/redirect/retry state leaking between overlapping navigations, restores correct SSR status codes for redirects, errors, and not-found responses, and closes hydration gaps where the client re-ran work the server had already completed.
+ - Invalidation now retires matching active preloads so older speculative loader results cannot become fresh cache data after invalidation.
+ - Route `headers()` now only runs on the server, matching the documented behavior — it is no longer invoked during client-side asset projection.
+ - The documented default `gcTime` and `preloadGcTime` now match the existing runtime default of 5 minutes (`300_000`).
+
+ **Removed / changed exported internals**
+ - `RouterState` no longer includes `loadedAt`, `isTransitioning`, `statusCode`, or `redirect`. Use `match.updatedAt` in place of `loadedAt`; subscribe to `router.state.status` / `router.state.isLoading` in place of `isTransitioning`; server response status and redirect handling are now internal to the server loader and are no longer exposed on `router.state`.
+ - `RouteMatch.fetchCount` has been removed, with no replacement — it was purely informational.
+ - `RouteMatch.status` no longer includes `'redirected'` (it remains `'pending' | 'success' | 'error' | 'notFound'`) — redirected matches are dropped from the match list instead of being rendered.
+ - `RouteMatch.globalNotFound` has been renamed and privatized to the internal `_notFound` field. Use `match.status === 'notFound'` instead.
+ - The exported React, Solid, and Vue `Match` components now accept `routeId` instead of `matchId`.
+ - The exported `RouterStores` adapter contract now uses route-keyed presentation stores: `matchesId` is replaced by `ids`, `matchStores` by `byRoute`, and `getRouteMatchStore()` by `getMatchStore()`. The separate `loadedAt`, `isLoading`, `isTransitioning`, `statusCode`, and `redirect` stores have been removed, along with the pending/cache stores and their setters. `StoreConfig.init` has also been removed. Read application-facing state from `router.state`; preload and cache coordination are now internal.
+ - Removed `RouterCore` members `getMatch()`, `updateMatch()`, `cancelMatch()`, and `cancelMatches()` — read matches from `router.state.matches` (e.g. `router.state.matches.find((m) => m.id === id)`); there is no replacement for mutating or cancelling an individual in-flight match from outside the router.
+ - Removed `RouterCore.hasNotFoundMatch()` — use `router.state.matches.some((m) => m.status === 'notFound')`.
+ - Removed `RouterCore.looseRoutesById` — use `routesById`.
+ - Removed `RouterCore.isPrerendering()`, `RouterCore.isViewTransitionTypesSupported`, and `RouterCore.viewTransitionPromise`, with no replacement.
+ - Removed `RouterCore.getParsedLocationHref()` and `RouterCore.clearExpiredCache()`, with no replacement — expired cache entries are now reconciled automatically as part of match commit.
+ - Removed `RouterCore.latestLoadPromise` and `RouterCore.beforeLoad()`, with no replacement.
+ - `RouterCore.commitLocationPromise` and `RouterCore.pendingBuiltLocation` have been replaced by the internal `_commitPromise` and `_pendingLocation` fields.
+ - Removed the exported `GetMatchFn` and `UpdateMatchFn` types, along with the methods they typed.
+ - Removed the standalone `getMatchedRoutes()` export from `@tanstack/router-core` — use the `router.getMatchedRoutes()` instance method instead.
+ - `RouterCore.loadRouteChunk()` no longer accepts an array of component types as its second argument. One-argument usage is unchanged; the optional second argument is now `'errorComponent'`, `'notFoundComponent'`, or `false` for internal boundary loading.
+ - Removed `Redirect.redirectHandled`, which was internal redirect bookkeeping.
+ - `MatchRoutesOpts.preload` and `MatchRoutesOpts.dest` have been removed.
+ - `StartTransitionFn` is now `(fn, expected) => Promise
` (previously `(fn) => void`). This only affects custom framework adapters that implement `startTransition`.
+
+- Updated dependencies [[`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd)]:
+ - @tanstack/router-core@1.171.16
+
+## 1.170.18
+
+### Patch Changes
+
+- Updated dependencies [[`e2dd204`](https://github.com/TanStack/router/commit/e2dd2049cb42eb219d3b447b8605066d19d9c1fa)]:
+ - @tanstack/router-core@1.171.15
+
## 1.170.17
### Patch Changes
diff --git a/packages/react-router/README.md b/packages/react-router/README.md
index 0a19fd3b81..c3a60d1008 100644
--- a/packages/react-router/README.md
+++ b/packages/react-router/README.md
@@ -2,7 +2,21 @@
# TanStack React Router
-
+
+
+
+
+
🤖 Type-safe router w/ built-in caching & URL state management for React!
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 2e63f8d973..fdb6266ef2 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-router",
- "version": "1.170.17",
+ "version": "1.170.19",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -27,12 +27,12 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js -p tsconfig.legacy.json",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js -p tsconfig.legacy.json",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js -p tsconfig.legacy.json",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js -p tsconfig.legacy.json",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js -p tsconfig.legacy.json",
- "test:types:ts60": "tsc -p tsconfig.legacy.json",
+ "test:types:ts60": "tsc6 -p tsconfig.legacy.json",
+ "test:types:ts70": "tsc -p tsconfig.legacy.json",
"test:unit": "vitest",
"test:unit:dev": "pnpm run test:unit --watch --hideSkippedTests",
"test:perf": "vitest bench",
@@ -100,6 +100,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
+ "@tanstack/react-query": "catalog:",
"@types/node": ">=20",
"@vitejs/plugin-react": "^4.3.4",
"combinate": "^1.1.11",
diff --git a/packages/react-router/skills/compositions/router-query/SKILL.md b/packages/react-router/skills/compositions/router-query/SKILL.md
index 89fba76b54..bcb2f939cd 100644
--- a/packages/react-router/skills/compositions/router-query/SKILL.md
+++ b/packages/react-router/skills/compositions/router-query/SKILL.md
@@ -1,14 +1,15 @@
---
-name: compositions/router-query
+name: router-query
description: >-
Integrating TanStack Router with TanStack Query: queryClient
in router context, ensureQueryData/prefetchQuery in loaders,
useSuspenseQuery in components, defaultPreloadStaleTime: 0,
setupRouterSsrQueryIntegration for SSR dehydration/hydration
and streaming, per-request QueryClient isolation.
-type: composition
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: composition
+ library: tanstack-router
+ library_version: '1.166.2'
requires:
- router-core
- router-core/data-loading
diff --git a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md
index c9f53358e6..fdc4f4c4e5 100644
--- a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md
+++ b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md
@@ -1,13 +1,14 @@
---
-name: lifecycle/migrate-from-react-router
+name: migrate-from-react-router
description: >-
Step-by-step migration from React Router v7 to TanStack Router:
route definition conversion, Link/useNavigate API differences,
useSearchParams to validateSearch + useSearch, useParams with from,
Outlet replacement, loader conversion, code splitting differences.
-type: lifecycle
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: lifecycle
+ library: tanstack-router
+ library_version: '1.166.2'
requires:
- router-core
- react-router
diff --git a/packages/react-router/skills/react-router/SKILL.md b/packages/react-router/skills/react-router/SKILL.md
index 074f23e091..53f4135316 100644
--- a/packages/react-router/skills/react-router/SKILL.md
+++ b/packages/react-router/skills/react-router/SKILL.md
@@ -8,10 +8,11 @@ description: >-
Outlet, CatchBoundary, ErrorComponent. React-specific patterns
for hooks, providers, SSR hydration, and createLink with
forwardRef.
-type: framework
-library: tanstack-router
-library_version: '1.166.2'
-framework: react
+metadata:
+ type: framework
+ library: tanstack-router
+ library_version: '1.166.2'
+ framework: react
requires:
- router-core
sources:
@@ -24,8 +25,6 @@ sources:
This skill builds on router-core. Read [router-core](../../../router-core/skills/router-core/SKILL.md) first for foundational concepts.
-This skill covers the React-specific bindings, components, hooks, and setup for TanStack Router.
-
> **CRITICAL**: TanStack Router types are FULLY INFERRED. Never cast, never annotate inferred values.
> **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, not on the server.
> **CRITICAL**: Do not confuse `@tanstack/react-router` with `react-router-dom`/`react-router`. They are completely different libraries with different APIs.
diff --git a/packages/react-router/src/CatchBoundary.tsx b/packages/react-router/src/CatchBoundary.tsx
index 98743f9447..d97e49f8f5 100644
--- a/packages/react-router/src/CatchBoundary.tsx
+++ b/packages/react-router/src/CatchBoundary.tsx
@@ -5,44 +5,25 @@ import type { ErrorRouteComponent } from './route'
import type { ErrorInfo } from 'react'
export function CatchBoundary(props: {
- getResetKey: () => number | string
+ getResetKey: () => unknown
children: React.ReactNode
errorComponent?: ErrorRouteComponent
onCatch?: (error: Error, errorInfo: ErrorInfo) => void
}) {
- const errorComponent = props.errorComponent ?? ErrorComponent
-
- return (
- {
- if (error) {
- return React.createElement(errorComponent, {
- error,
- reset,
- })
- }
-
- return props.children
- }}
- />
- )
+ return
}
class CatchBoundaryImpl extends React.Component<{
- getResetKey: () => number | string
- children: (props: {
- error: Error | null
- reset: () => void
- }) => React.ReactNode
+ getResetKey: () => unknown
+ children: React.ReactNode
+ errorComponent?: ErrorRouteComponent
onCatch?: (error: Error, errorInfo: ErrorInfo) => void
}> {
- state = { error: null } as { error: Error | null; resetKey?: string | number }
+ state = { error: null } as { error: Error | null; resetKey?: unknown }
static getDerivedStateFromProps(
- props: { getResetKey: () => string | number },
- state: { resetKey?: string | number; error: Error | null },
+ props: { getResetKey: () => unknown },
+ state: { resetKey?: unknown; error: Error | null },
) {
const resetKey = props.getResetKey()
@@ -55,21 +36,20 @@ class CatchBoundaryImpl extends React.Component<{
static getDerivedStateFromError(error: Error) {
return { error }
}
- reset() {
+ reset = () => {
this.setState({ error: null })
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
- if (this.props.onCatch) {
- this.props.onCatch(error, errorInfo)
- }
+ this.props.onCatch?.(error, errorInfo)
}
render() {
- return this.props.children({
- error: this.state.error,
- reset: () => {
- this.reset()
- },
- })
+ const error = this.state.error
+ return error
+ ? React.createElement(this.props.errorComponent ?? ErrorComponent, {
+ error,
+ reset: this.reset,
+ })
+ : this.props.children
}
}
diff --git a/packages/react-router/src/ClientOnly.tsx b/packages/react-router/src/ClientOnly.tsx
index 10f903b204..102859d75a 100644
--- a/packages/react-router/src/ClientOnly.tsx
+++ b/packages/react-router/src/ClientOnly.tsx
@@ -31,11 +31,7 @@ export interface ClientOnlyProps {
* ```
*/
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
- return useHydrated() ? (
- {children}
- ) : (
- {fallback}
- )
+ return {useHydrated() ? children : fallback}
}
/**
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index 5de5546aeb..6bca031a4d 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -2,14 +2,7 @@
import * as React from 'react'
import { useStore } from '@tanstack/react-store'
-import {
- createControlledPromise,
- getLocationChangeInfo,
- invariant,
- isNotFound,
- isRedirect,
- rootRouteId,
-} from '@tanstack/router-core'
+import { isNotFound, rootRouteId } from '@tanstack/router-core'
import { isServer } from '@tanstack/router-core/isServer'
import { CatchBoundary, ErrorComponent } from './CatchBoundary'
import { useRouter } from './useRouter'
@@ -19,131 +12,59 @@ import { SafeFragment } from './SafeFragment'
import { renderRouteNotFound } from './renderRouteNotFound'
import { ScrollRestoration } from './scroll-restoration'
import { ClientOnly } from './ClientOnly'
-import { useLayoutEffect } from './utils'
import type {
AnyRoute,
AnyRouteMatch,
- ParsedLocation,
RootRouteOptions,
} from '@tanstack/router-core'
+export function renderPending(
+ router: ReturnType,
+ route?: AnyRoute,
+) {
+ const PendingComponent =
+ route?.options.pendingComponent ?? router.options.defaultPendingComponent
+ return PendingComponent ? : null
+}
+
type OutletMatchSelection = [
- routeId: string | undefined,
parentGlobalNotFound: boolean,
+ parentNotFoundError: unknown,
]
-const matchViewFieldsEqual = (a: AnyRouteMatch, b: AnyRouteMatch) =>
- a.routeId === b.routeId && a._displayPending === b._displayPending
-
const outletMatchSelectionEqual = (
a: OutletMatchSelection,
b: OutletMatchSelection,
) => a[0] === b[0] && a[1] === b[1]
export const Match = React.memo(function MatchImpl({
- matchId,
+ routeId,
}: {
- matchId: string
+ routeId: string
}) {
const router = useRouter()
if (isServer ?? router.isServer) {
- const match = router.stores.matchStores.get(matchId)?.get()
- if (!match) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error(
- `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
- )
- }
-
- invariant()
- }
-
- const routeId = match.routeId as string
- const parentRouteId = (router.routesById[routeId] as AnyRoute).parentRoute
- ?.id
-
- return (
-
- )
+ const match = router.stores.byRoute.get(routeId)!.get()!
+ return
}
- // Subscribe directly to the match store from the pool.
- // The matchId prop is stable for this component's lifetime (set by Outlet),
- // and reconcileMatchPool reuses stores for the same matchId.
-
- const matchStore = router.stores.matchStores.get(matchId)
- if (!matchStore) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error(
- `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
- )
- }
-
- invariant()
- }
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const resetKey = useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
+ const matchStore = router.stores.getMatchStore(routeId)
// eslint-disable-next-line react-hooks/rules-of-hooks
- const match = useStore(matchStore, (value) => value, matchViewFieldsEqual)
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const matchState = React.useMemo(() => {
- const routeId = match.routeId as string
- const parentRouteId = (router.routesById[routeId] as AnyRoute).parentRoute
- ?.id
-
- return {
- routeId,
- ssr: match.ssr,
- _displayPending: match._displayPending,
- parentRouteId: parentRouteId as string | undefined,
- } satisfies MatchViewState
- }, [match._displayPending, match.routeId, match.ssr, router.routesById])
-
- return (
-
- )
+ const match = useStore(matchStore, (value) => value)
+ return
})
-type MatchViewState = {
- routeId: string
- ssr: boolean | 'data-only' | undefined
- _displayPending: boolean | undefined
- parentRouteId: string | undefined
-}
-
function MatchView({
router,
- matchId,
- resetKey,
- matchState,
+ match,
}: {
router: ReturnType
- matchId: string
- resetKey: number
- matchState: MatchViewState
+ match: AnyRouteMatch
}) {
- const route: AnyRoute = router.routesById[matchState.routeId]
+ const route: AnyRoute = router.routesById[match.routeId]
- const PendingComponent =
- route.options.pendingComponent ?? router.options.defaultPendingComponent
-
- const pendingElement = PendingComponent ? : null
+ const pendingElement = renderPending(router, route)
const routeErrorComponent =
route.options.errorComponent ?? router.options.defaultErrorComponent
@@ -151,19 +72,16 @@ function MatchView({
const routeOnCatch = route.options.onCatch ?? router.options.defaultOnCatch
const routeNotFoundComponent = route.isRoot
- ? // If it's the root route, use the globalNotFound option, with fallback to the notFoundRoute's component
+ ? // If it's the root route, use the _notFound option, with fallback to the notFoundRoute's component
(route.options.notFoundComponent ??
router.options.notFoundRoute?.options.component)
: route.options.notFoundComponent
- const resolvedNoSsr =
- matchState.ssr === false || matchState.ssr === 'data-only'
+ const resolvedNoSsr = match.ssr === false || match.ssr === 'data-only'
const ResolvedSuspenseBoundary =
- // If we're on the root route, allow forcefully wrapping in suspense
- (!route.isRoot || route.options.wrapInSuspense || resolvedNoSsr) &&
(route.options.wrapInSuspense ??
- PendingComponent ??
- ((route.options.errorComponent as any)?.preload || resolvedNoSsr))
+ pendingElement ??
+ ((route.options.errorComponent as any)?.preload || resolvedNoSsr))
? React.Suspense
: SafeFragment
@@ -180,235 +98,68 @@ function MatchView({
: SafeFragment
return (
-
+
resetKey}
- errorComponent={routeErrorComponent || ErrorComponent}
+ getResetKey={() => match}
+ errorComponent={routeErrorComponent as any}
onCatch={(error, errorInfo) => {
// Forward not found errors (we don't want to show the error component for these)
if (isNotFound(error)) {
- error.routeId ??= matchState.routeId as any
+ error.routeId ??= match.routeId
throw error
}
if (process.env.NODE_ENV !== 'production') {
- console.warn(`Warning: Error in route match: ${matchId}`)
+ console.warn(`Warning: Error in route match: ${match.id}`)
}
routeOnCatch?.(error, errorInfo)
}}
>
{
- error.routeId ??= matchState.routeId as any
-
- // If the current not found handler doesn't exist or it has a
- // route ID which doesn't match the current route, rethrow the error
- if (
- !routeNotFoundComponent ||
- (error.routeId && error.routeId !== matchState.routeId) ||
- (!error.routeId && !route.isRoot)
- )
+ error.routeId ??= match.routeId
+
+ if (error.routeId !== match.routeId) {
throw error
+ }
- return React.createElement(routeNotFoundComponent, error as any)
+ return React.createElement(
+ routeNotFoundComponent!,
+ error as any,
+ )
}}
>
- {resolvedNoSsr || matchState._displayPending ? (
+ {resolvedNoSsr ? (
-
+
) : (
-
+
)}
- {matchState.parentRouteId === rootRouteId ? (
- <>
-
- {router.options.scrollRestoration && (isServer ?? router.isServer) ? (
-
- ) : null}
- >
+ {(isServer ?? router.isServer) &&
+ route.parentRoute?.id === rootRouteId &&
+ router.options.scrollRestoration ? (
+
) : null}
)
}
-// On Rendered can't happen above the root layout because it needs to run after
-// the route subtree has committed below the root layout. Keeping it here lets
-// us fire onRendered even after a hydration mismatch above the root layout
-// (like bad head/link tags, which is common).
-function OnRendered() {
- const router = useRouter()
-
- if (isServer ?? router.isServer) {
- return null
- }
-
- // Track the resolvedLocation as of the last render so that onRendered can
- // report the correct fromLocation. By the time this effect fires,
- // resolvedLocation has already been updated to the new location by
- // Transitioner, so we cannot use router.stores.resolvedLocation.get()
- // directly as the fromLocation.
- // @ts-expect-error -- init to `undefined` but don't write `undefined` to shave bytes
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const prevResolvedLocationRef = React.useRef<
- ParsedLocation | undefined
- >()
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const renderedLocationKey = useStore(
- router.stores.resolvedLocation,
- (resolvedLocation) => resolvedLocation?.state.__TSR_key,
- )
-
- // eslint-disable-next-line react-hooks/rules-of-hooks
- useLayoutEffect(() => {
- const currentResolvedLocation = router.stores.resolvedLocation.get()
- const previousResolvedLocation = prevResolvedLocationRef.current
-
- if (
- currentResolvedLocation &&
- (!previousResolvedLocation ||
- previousResolvedLocation.href !== currentResolvedLocation.href)
- ) {
- router.emit({
- type: 'onRendered',
- ...getLocationChangeInfo(
- router.stores.location.get(),
- previousResolvedLocation ?? currentResolvedLocation,
- ),
- })
- }
- prevResolvedLocationRef.current = currentResolvedLocation
- }, [renderedLocationKey, router])
-
- return null
-}
-
export const MatchInner = React.memo(function MatchInnerImpl({
- matchId,
+ match,
}: {
- matchId: string
+ match: AnyRouteMatch
}): any {
const router = useRouter()
-
- const getMatchPromise = (
- match: {
- id: string
- _nonReactive: {
- displayPendingPromise?: Promise
- minPendingPromise?: Promise
- loadPromise?: Promise
- }
- },
- key: 'displayPendingPromise' | 'minPendingPromise' | 'loadPromise',
- ) => {
- return (
- router.getMatch(match.id)?._nonReactive[key] ?? match._nonReactive[key]
- )
- }
-
- if (isServer ?? router.isServer) {
- const match = router.stores.matchStores.get(matchId)?.get()
- if (!match) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error(
- `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
- )
- }
-
- invariant()
- }
-
- const routeId = match.routeId as string
- const route = router.routesById[routeId] as AnyRoute
- const remountFn =
- (router.routesById[routeId] as AnyRoute).options.remountDeps ??
- router.options.defaultRemountDeps
- const remountDeps = remountFn?.({
- routeId,
- loaderDeps: match.loaderDeps,
- params: match._strictParams,
- search: match._strictSearch,
- })
- const key = remountDeps ? JSON.stringify(remountDeps) : undefined
- const Comp = route.options.component ?? router.options.defaultComponent
- const out = Comp ? :
-
- if (match._displayPending) {
- throw getMatchPromise(match, 'displayPendingPromise')
- }
-
- if (match._forcePending) {
- throw getMatchPromise(match, 'minPendingPromise')
- }
-
- if (match.status === 'pending') {
- throw getMatchPromise(match, 'loadPromise')
- }
-
- if (match.status === 'notFound') {
- if (!isNotFound(match.error)) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error('Invariant failed: Expected a notFound error')
- }
-
- invariant()
- }
- return renderRouteNotFound(router, route, match.error)
- }
-
- if (match.status === 'redirected') {
- if (!isRedirect(match.error)) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error('Invariant failed: Expected a redirect error')
- }
-
- invariant()
- }
- throw getMatchPromise(match, 'loadPromise')
- }
-
- if (match.status === 'error') {
- const RouteErrorComponent =
- (route.options.errorComponent ??
- router.options.defaultErrorComponent) ||
- ErrorComponent
- return (
-
- )
- }
-
- return out
- }
-
- const matchStore = router.stores.matchStores.get(matchId)
- if (!matchStore) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error(
- `Invariant failed: Could not find match for matchId "${matchId}". Please file an issue!`,
- )
- }
-
- invariant()
- }
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const match = useStore(matchStore, (value) => value)
- const routeId = match.routeId as string
+ const routeId = match.routeId
const route = router.routesById[routeId] as AnyRoute
- // eslint-disable-next-line react-hooks/rules-of-hooks
const key = React.useMemo(() => {
const remountFn =
- (router.routesById[routeId] as AnyRoute).options.remountDeps ??
- router.options.defaultRemountDeps
+ route.options.remountDeps ?? router.options.defaultRemountDeps
const remountDeps = remountFn?.({
routeId,
loaderDeps: match.loaderDeps,
@@ -421,85 +172,26 @@ export const MatchInner = React.memo(function MatchInnerImpl({
match.loaderDeps,
match._strictParams,
match._strictSearch,
+ route.options.remountDeps,
router.options.defaultRemountDeps,
- router.routesById,
])
-
- // eslint-disable-next-line react-hooks/rules-of-hooks
const out = React.useMemo(() => {
const Comp = route.options.component ?? router.options.defaultComponent
- if (Comp) {
- return
- }
- return
+ return Comp ? :
}, [key, route.options.component, router.options.defaultComponent])
- if (match._displayPending) {
- throw getMatchPromise(match, 'displayPendingPromise')
- }
-
- if (match._forcePending) {
- throw getMatchPromise(match, 'minPendingPromise')
- }
-
- // see also hydrate() in packages/router-core/src/ssr/ssr-client.ts
if (match.status === 'pending') {
- // We're pending, and if we have a minPendingMs, we need to wait for it
- const pendingMinMs =
- route.options.pendingMinMs ?? router.options.defaultPendingMinMs
- if (pendingMinMs) {
- const routerMatch = router.getMatch(match.id)
- if (routerMatch && !routerMatch._nonReactive.minPendingPromise) {
- // Create a promise that will resolve after the minPendingMs
- if (!(isServer ?? router.isServer)) {
- const minPendingPromise = createControlledPromise()
-
- routerMatch._nonReactive.minPendingPromise = minPendingPromise
-
- setTimeout(() => {
- minPendingPromise.resolve()
- // We've handled the minPendingPromise, so we can delete it
- routerMatch._nonReactive.minPendingPromise = undefined
- }, pendingMinMs)
- }
- }
+ if (router._tx) {
+ throw router._tx[5]
}
- throw getMatchPromise(match, 'loadPromise')
+ return renderPending(router, route)
}
if (match.status === 'notFound') {
- if (!isNotFound(match.error)) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error('Invariant failed: Expected a notFound error')
- }
-
- invariant()
- }
return renderRouteNotFound(router, route, match.error)
}
- if (match.status === 'redirected') {
- // A match can be observed as redirected during an in-flight transition,
- // especially when pending UI is already rendering. Suspend on the match's
- // load promise so React can abandon this stale render and continue the
- // redirect transition.
- if (!isRedirect(match.error)) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error('Invariant failed: Expected a redirect error')
- }
-
- invariant()
- }
-
- throw getMatchPromise(match, 'loadPromise')
- }
-
if (match.status === 'error') {
- // If we're on the server, we need to use React's new and super
- // wonky api for throwing errors from a server side render inside
- // of a suspense boundary. This is the only way to get
- // renderToPipeableStream to not hang indefinitely.
- // We'll serialize the error and rethrow it on the client.
if (isServer ?? router.isServer) {
const RouteErrorComponent =
(route.options.errorComponent ??
@@ -515,7 +207,6 @@ export const MatchInner = React.memo(function MatchInnerImpl({
/>
)
}
-
throw match.error
}
@@ -530,74 +221,54 @@ export const MatchInner = React.memo(function MatchInnerImpl({
*/
export const Outlet = React.memo(function OutletImpl() {
const router = useRouter()
- const matchId = React.useContext(matchContext)
+ const routeId = React.useContext(matchContext)!
- let routeId: string | undefined
- let parentGlobalNotFound = false
- let childMatchId: string | undefined
+ let parentGlobalNotFound: boolean
+ let parentNotFoundError: unknown
+ let childRouteId: string | undefined
if (isServer ?? router.isServer) {
const matches = router.stores.matches.get()
- const parentIndex = matchId
- ? matches.findIndex((match) => match.id === matchId)
- : -1
- const parentMatch = parentIndex >= 0 ? matches[parentIndex] : undefined
- routeId = parentMatch?.routeId as string | undefined
- parentGlobalNotFound = parentMatch?.globalNotFound ?? false
- childMatchId =
- parentIndex >= 0 ? (matches[parentIndex + 1]?.id as string) : undefined
+ const parentIndex = matches.findIndex((match) => match.routeId === routeId)
+ const parentMatch = matches[parentIndex]!
+ parentGlobalNotFound = !!parentMatch._notFound
+ parentNotFoundError = parentMatch.error
+ childRouteId = matches[parentIndex + 1]?.routeId
} else {
- // Subscribe directly to the match store from the pool instead of
- // the two-level byId → matchStore pattern.
- const parentMatchStore = matchId
- ? router.stores.matchStores.get(matchId)
- : undefined
+ const parentMatchStore = router.stores.getMatchStore(routeId)
// eslint-disable-next-line react-hooks/rules-of-hooks
- ;[routeId, parentGlobalNotFound] = useStore(
+ ;[parentGlobalNotFound, parentNotFoundError] = useStore(
parentMatchStore,
- (match): OutletMatchSelection => [
- match?.routeId as string | undefined,
- match?.globalNotFound ?? false,
- ],
+ (match): OutletMatchSelection => [!!match!._notFound, match!.error],
outletMatchSelectionEqual,
)
// eslint-disable-next-line react-hooks/rules-of-hooks
- childMatchId = useStore(router.stores.matchesId, (ids) => {
- const index = ids.findIndex((id) => id === matchId)
- return ids[index + 1]
+ childRouteId = useStore(router.stores.ids, (ids) => {
+ return ids[ids.indexOf(routeId) + 1]
})
}
- const route = routeId ? router.routesById[routeId] : undefined
-
- const pendingElement = router.options.defaultPendingComponent ? (
-
- ) : null
-
if (parentGlobalNotFound) {
- if (!route) {
- if (process.env.NODE_ENV !== 'production') {
- throw new Error(
- 'Invariant failed: Could not resolve route for Outlet render',
- )
- }
-
- invariant()
- }
- return renderRouteNotFound(router, route, undefined)
+ return renderRouteNotFound(
+ router,
+ router.routesById[routeId],
+ parentNotFoundError,
+ )
}
- if (!childMatchId) {
+ if (!childRouteId) {
return null
}
- const nextMatch =
+ const nextMatch =
if (routeId === rootRouteId) {
return (
- {nextMatch}
+
+ {nextMatch}
+
)
}
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index 62d0fc42ae..3be4173996 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -4,12 +4,13 @@ import * as React from 'react'
import { useStore } from '@tanstack/react-store'
import { rootRouteId } from '@tanstack/router-core'
import { isServer } from '@tanstack/router-core/isServer'
-import { CatchBoundary, ErrorComponent } from './CatchBoundary'
+import { CatchBoundary } from './CatchBoundary'
import { useRouter } from './useRouter'
import { useStructuralSharing } from './useMatch'
-import { Transitioner } from './Transitioner'
+import { useLayoutEffect } from './utils'
+import { Transitioner, settleOwner } from './Transitioner'
import { matchContext } from './matchContext'
-import { Match } from './Match'
+import { Match, renderPending } from './Match'
import { SafeFragment } from './SafeFragment'
import type {
StructuralSharingOption,
@@ -48,23 +49,19 @@ export function Matches() {
const router = useRouter()
const rootRoute: AnyRoute = router.routesById[rootRouteId]
- const PendingComponent =
- rootRoute.options.pendingComponent ?? router.options.defaultPendingComponent
-
- const pendingElement = PendingComponent ? : null
+ const pendingElement = renderPending(router, rootRoute)
// Do not render a root Suspense during SSR or hydrating from SSR
const ResolvedSuspense =
- (isServer ?? router.isServer) ||
- (typeof document !== 'undefined' && router.ssr)
- ? SafeFragment
- : React.Suspense
+ (isServer ?? router.isServer) || router.ssr ? SafeFragment : React.Suspense
const inner = (
-
+ <>
{!(isServer ?? router.isServer) && }
-
-
+
+
+
+ >
)
return router.options.InnerWrap ? (
@@ -76,26 +73,33 @@ export function Matches() {
function MatchesInner() {
const router = useRouter()
- const _isServer = isServer ?? router.isServer
- const matchId = _isServer
- ? router.stores.firstId.get()
- : // eslint-disable-next-line react-hooks/rules-of-hooks
- useStore(router.stores.firstId, (id) => id)
- const resetKey = _isServer
- ? router.stores.loadedAt.get()
- : // eslint-disable-next-line react-hooks/rules-of-hooks
- useStore(router.stores.loadedAt, (loadedAt) => loadedAt)
+ const acknowledgement = router._rendered!
+ const matches =
+ (isServer ?? router.isServer)
+ ? router.stores.matches.get()
+ : // eslint-disable-next-line react-hooks/rules-of-hooks
+ useStore(
+ router.stores.matches,
+ (value) => acknowledgement[0 /* offered */] ?? value,
+ )
+ const match = matches[0]
+ const routeId = match?.routeId
+
+ useLayoutEffect(() => {
+ if (acknowledgement[0 /* offered */] === matches) {
+ settleOwner(acknowledgement, true)
+ }
+ }, [acknowledgement, matches])
- const matchComponent = matchId ? : null
+ const matchComponent = routeId ? : null
return (
-
+
{router.options.disableGlobalCatchBoundary ? (
matchComponent
) : (
resetKey}
- errorComponent={ErrorComponent}
+ getResetKey={() => match}
onCatch={
process.env.NODE_ENV !== 'production'
? (error) => {
@@ -143,7 +147,11 @@ export function useMatchRoute() {
if (!(isServer ?? router.isServer)) {
// eslint-disable-next-line react-hooks/rules-of-hooks
- useStore(router.stores.matchRouteDeps, (d) => d)
+ useStore(router.stores.location, (location) => location.href)
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useStore(router.stores.resolvedLocation, (location) => location?.href)
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useStore(router.stores.status, (status) => status)
}
return React.useCallback(
@@ -255,20 +263,8 @@ export function useMatches<
}
/**
- * Read the full array of active route matches or select a derived subset.
- *
- * Useful for debugging, breadcrumbs, or aggregating metadata across matches.
- *
- * @returns The array of matches (or the selected value).
- * @link https://tanstack.com/router/latest/docs/framework/react/api/router/useMatchesHook
- */
-
-/**
- * Read the full array of active route matches or select a derived subset.
- *
- * Useful for debugging, breadcrumbs, or aggregating metadata across matches.
- *
- * @link https://tanstack.com/router/latest/docs/framework/react/api/router/useMatchesHook
+ * Read the presented route matches above the current match, or select a
+ * derived value from them.
*/
export function useParentMatches<
TRouter extends AnyRouter = RegisteredRouter,
@@ -278,13 +274,13 @@ export function useParentMatches<
opts?: UseMatchesBaseOptions &
StructuralSharingOption,
): UseMatchesResult {
- const contextMatchId = React.useContext(matchContext)
+ const contextRouteId = React.useContext(matchContext)
return useMatches({
select: (matches: Array>) => {
matches = matches.slice(
0,
- matches.findIndex((d) => d.id === contextMatchId),
+ matches.findIndex((d) => d.routeId === contextRouteId),
)
return opts?.select ? opts.select(matches) : matches
},
@@ -293,8 +289,8 @@ export function useParentMatches<
}
/**
- * Read the array of active route matches that are children of the current
- * match (or selected parent) in the match tree.
+ * Read the presented route matches below the current match, or select a
+ * derived value from them.
*/
export function useChildMatches<
TRouter extends AnyRouter = RegisteredRouter,
@@ -304,12 +300,12 @@ export function useChildMatches<
opts?: UseMatchesBaseOptions &
StructuralSharingOption,
): UseMatchesResult {
- const contextMatchId = React.useContext(matchContext)
+ const contextRouteId = React.useContext(matchContext)
return useMatches({
select: (matches: Array>) => {
matches = matches.slice(
- matches.findIndex((d) => d.id === contextMatchId) + 1,
+ matches.findIndex((d) => d.routeId === contextRouteId) + 1,
)
return opts?.select ? opts.select(matches) : matches
},
diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index 4846ab17fc..81c3fb4ece 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -50,8 +50,8 @@ export function RouterContextProvider<
}
/**
- * Top-level component that renders the active route matches and provides the
- * router to the React tree via context.
+ * Renders the current match presentation and provides the router to the React
+ * tree via context.
*
* Accepts the same options as `createRouter` via props to update the router
* instance after creation.
diff --git a/packages/react-router/src/Scripts.tsx b/packages/react-router/src/Scripts.tsx
index 5285fbd8db..a1b189d4c6 100644
--- a/packages/react-router/src/Scripts.tsx
+++ b/packages/react-router/src/Scripts.tsx
@@ -1,5 +1,5 @@
import { useStore } from '@tanstack/react-store'
-import { deepEqual } from '@tanstack/router-core'
+import { _getAssetMatches, deepEqual } from '@tanstack/router-core'
import { isServer } from '@tanstack/router-core/isServer'
import { Asset } from './Asset'
import { useRouter } from './useRouter'
@@ -17,23 +17,38 @@ export const Scripts = () => {
const router = useRouter()
const nonce = router.options.ssr?.nonce
- const getAssetScripts = (matches: Array) => {
- const assetScripts: Array = []
+ const getScripts = (matches: Array) => {
+ matches = _getAssetMatches(matches)
+ const scripts = matches
+ .flatMap((match) => match.scripts ?? [])
+ .filter(Boolean)
+ .map(
+ ({ children, ...script }) =>
+ ({
+ tag: 'script',
+ attrs: {
+ ...script,
+ suppressHydrationWarning: true,
+ nonce,
+ },
+ children,
+ }) satisfies RouterManagedTag,
+ ) as Array
const manifest = router.ssr?.manifest
if (!manifest) {
- return []
+ return scripts
}
for (const match of matches) {
- const scripts = manifest.routes[match.routeId]?.scripts
+ const manifestScripts = manifest.routes[match.routeId]?.scripts
- if (!scripts) {
+ if (!manifestScripts) {
continue
}
- for (const asset of scripts) {
- assetScripts.push({
+ for (const asset of manifestScripts) {
+ scripts.push({
tag: 'script',
attrs: { ...asset.attrs, nonce },
children: asset.children,
@@ -44,64 +59,35 @@ export const Scripts = () => {
}
}
- return assetScripts
+ return scripts
}
- const getScripts = (matches: Array): Array =>
- (
- matches
- .map((match) => match.scripts!)
- .flat(1)
- .filter(Boolean) as Array
- ).map(
- ({ children, ...script }) =>
- ({
- tag: 'script',
- attrs: {
- ...script,
- suppressHydrationWarning: true,
- nonce,
- },
- children,
- }) satisfies RouterManagedTag,
- )
-
if (isServer ?? router.isServer) {
const activeMatches = router.stores.matches.get()
- const assetScripts = getAssetScripts(activeMatches)
const scripts = getScripts(activeMatches)
- return renderScripts(router, scripts, assetScripts)
+ return renderScripts(router, scripts)
}
- // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const assetScripts = useStore(
- router.stores.matches,
- getAssetScripts,
- deepEqual,
- )
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
const scripts = useStore(router.stores.matches, getScripts, deepEqual)
- return renderScripts(router, scripts, assetScripts)
+ return renderScripts(router, scripts)
}
function renderScripts(
router: ReturnType,
- scripts: Array,
- assetScripts: Array,
+ scripts: Array,
) {
- const allScripts = [...scripts, ...assetScripts] as Array
-
if ((isServer ?? router.isServer) && router.serverSsr) {
const serverBufferedScript = router.serverSsr.takeBufferedScripts()
if (serverBufferedScript) {
- allScripts.unshift(serverBufferedScript)
+ scripts.unshift(serverBufferedScript)
}
}
return (
<>
- {allScripts.map((asset, i) => (
+ {scripts.map((asset, i) => (
))}
>
diff --git a/packages/react-router/src/Transitioner.tsx b/packages/react-router/src/Transitioner.tsx
index bf945571a6..341d09ffcc 100644
--- a/packages/react-router/src/Transitioner.tsx
+++ b/packages/react-router/src/Transitioner.tsx
@@ -1,43 +1,65 @@
'use client'
import * as React from 'react'
-import { batch, useStore } from '@tanstack/react-store'
import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core'
-import { useLayoutEffect, usePrevious } from './utils'
+import { useLayoutEffect } from './utils'
import { useRouter } from './useRouter'
+import type { AnyRouter } from '@tanstack/router-core'
+
+export function settleOwner(
+ owner: NonNullable,
+ rendered: boolean,
+) {
+ const settle = owner[1 /* settle */]
+ owner.length = 0
+ settle?.(rendered)
+}
export function Transitioner() {
const router = useRouter()
- const mountLoadForRouter = React.useRef({ router, mounted: false })
-
- const [isTransitioning, setIsTransitioning] = React.useState(false)
- // Track pending state changes
- const isLoading = useStore(router.stores.isLoading, (value) => value)
- const hasPending = useStore(router.stores.hasPending, (value) => value)
-
- const previousIsLoading = usePrevious(isLoading)
-
- const isAnyPending = isLoading || isTransitioning || hasPending
- const previousIsAnyPending = usePrevious(isAnyPending)
-
- const isPagePending = isLoading || hasPending
- const previousIsPagePending = usePrevious(isPagePending)
-
- router.startTransition = (fn: () => void) => {
- setIsTransitioning(true)
- React.startTransition(() => {
- fn()
- setIsTransitioning(false)
+ const acknowledgement = (router._rendered ??= [])
+ const mounted =
+ process.env.NODE_ENV !== 'production'
+ ? // eslint-disable-next-line react-hooks/rules-of-hooks
+ React.useRef(false)
+ : undefined
+
+ router.startTransition = (fn, expected) =>
+ new Promise((resolve, reject) => {
+ settleOwner(acknowledgement, false)
+ acknowledgement.push(expected, resolve)
+ React.startTransition(() => {
+ try {
+ fn()
+ } catch (cause) {
+ if (acknowledgement[1 /* settle */] === resolve) {
+ acknowledgement.length = 0
+ }
+ reject(cause)
+ }
+ })
})
+ if (process.env.NODE_ENV !== 'production') {
+ ;(
+ router as typeof router & { _cancelTransition?: () => void }
+ )._cancelTransition = () => settleOwner(acknowledgement, false)
}
- // Subscribe to location changes
- // and try to load the new location
- React.useEffect(() => {
+ // Subscribe before canonicalizing so the initial URL has exactly one load.
+ useLayoutEffect(() => {
const unsub = router.history.subscribe(router.load)
+ if (mounted?.current) {
+ return unsub
+ }
+ if (mounted) {
+ mounted.current = true
+ }
+
+ router.updateLatestLocation()
+ const location = router.latestLocation
const nextLocation = router.buildLocation({
- to: router.latestLocation.pathname,
+ to: location.pathname,
search: true,
params: true,
hash: true,
@@ -46,86 +68,41 @@ export function Transitioner() {
})
// Check if the current URL matches the canonical form.
- // Compare publicHref (browser-facing URL) for consistency with
- // the server-side redirect check in router.beforeLoad.
+ // Compare publicHref (browser-facing URL) consistently with server
+ // canonicalization.
if (
- trimPathRight(router.latestLocation.publicHref) !==
+ trimPathRight(location.publicHref) !==
trimPathRight(nextLocation.publicHref)
) {
- router.commitLocation({ ...nextLocation, replace: true })
- }
-
- return () => {
- unsub()
+ router.commitLocation({
+ ...nextLocation,
+ replace: true,
+ ignoreBlocker: true,
+ })
+ return unsub
}
- }, [router, router.history])
- // Try to load the initial location
- useLayoutEffect(() => {
+ const resolvedLocation = router.stores.resolvedLocation.get()
if (
- // if we are hydrating from SSR, loading is triggered in ssr-client
- (typeof window !== 'undefined' && router.ssr) ||
- (mountLoadForRouter.current.router === router &&
- mountLoadForRouter.current.mounted)
+ resolvedLocation?.href === location.href &&
+ resolvedLocation.state.__TSR_key === location.state.__TSR_key
) {
- return
- }
- mountLoadForRouter.current = { router, mounted: true }
-
- const tryLoad = async () => {
- try {
- await router.load()
- } catch (err) {
- console.error(err)
- }
- }
-
- tryLoad()
- }, [router])
-
- useLayoutEffect(() => {
- // The router was loading and now it's not
- if (previousIsLoading && !isLoading) {
- router.emit({
- type: 'onLoad', // When the new URL has committed, when the new matches have been loaded into state.matches
- ...getLocationChangeInfo(
- router.stores.location.get(),
- router.stores.resolvedLocation.get(),
- ),
- })
- }
- }, [previousIsLoading, router, isLoading])
-
- useLayoutEffect(() => {
- // emit onBeforeRouteMount
- if (previousIsPagePending && !isPagePending) {
- router.emit({
- type: 'onBeforeRouteMount',
- ...getLocationChangeInfo(
- router.stores.location.get(),
- router.stores.resolvedLocation.get(),
- ),
+ acknowledgement.push(router.stores.matches.get(), (rendered) => {
+ if (rendered) {
+ router.emit({
+ type: 'onRendered',
+ ...getLocationChangeInfo(resolvedLocation, resolvedLocation),
+ })
+ }
})
+ } else if (!router._tx) {
+ router.load().catch(console.error)
}
- }, [isPagePending, previousIsPagePending, router])
-
- useLayoutEffect(() => {
- if (previousIsAnyPending && !isAnyPending) {
- const changeInfo = getLocationChangeInfo(
- router.stores.location.get(),
- router.stores.resolvedLocation.get(),
- )
- router.emit({
- type: 'onResolved',
- ...changeInfo,
- })
- batch(() => {
- router.stores.status.set('idle')
- router.stores.resolvedLocation.set(router.stores.location.get())
- })
- }
- }, [isAnyPending, previousIsAnyPending, router])
+ return unsub
+ // `mounted` exists only in development and is a stable ref when present.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [router, router.history])
return null
}
diff --git a/packages/react-router/src/headContentUtils.tsx b/packages/react-router/src/headContentUtils.tsx
index 5ed35bd1ce..d61ca829db 100644
--- a/packages/react-router/src/headContentUtils.tsx
+++ b/packages/react-router/src/headContentUtils.tsx
@@ -1,6 +1,7 @@
import * as React from 'react'
import { useStore } from '@tanstack/react-store'
import {
+ _getAssetMatches,
appendUniqueUserTags,
deepEqual,
escapeHtml,
@@ -22,6 +23,7 @@ function buildTagsFromMatches(
matches: Array,
assetCrossOrigin?: AssetCrossOriginConfig,
): Array {
+ matches = _getAssetMatches(matches)
const routeMeta = matches
.map((match) => match.meta)
.filter((meta) => meta !== undefined)
@@ -187,7 +189,7 @@ function buildTagsFromMatches(
}
/**
- * Build the list of head/link/meta/script tags to render for active matches.
+ * Build the head/link/meta/script tags from the renderable presented prefix.
* Used internally by `HeadContent`.
*/
export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
@@ -204,226 +206,11 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
}
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const routeMeta = useStore(
- router.stores.matches,
- (matches) => {
- return matches
- .map((match) => match.meta)
- .filter((meta) => meta !== undefined)
- },
- deepEqual,
+ const selectTags = React.useCallback(
+ (matches: Array) =>
+ buildTagsFromMatches(router, nonce, matches, assetCrossOrigin),
+ [assetCrossOrigin, nonce, router],
)
-
- // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const meta: Array = React.useMemo(() => {
- const resultMeta: Array = []
- const metaByAttribute: Record = {}
- let title: RouterManagedTag | undefined
- for (let i = routeMeta.length - 1; i >= 0; i--) {
- const metas = routeMeta[i]!
- for (let j = metas.length - 1; j >= 0; j--) {
- const m = metas[j]
- if (!m) continue
-
- if (m.title) {
- if (!title) {
- title = {
- tag: 'title',
- children: m.title,
- }
- }
- } else if ('script:ld+json' in m) {
- // Handle JSON-LD structured data
- // Content is HTML-escaped to prevent XSS when injected via dangerouslySetInnerHTML
- try {
- const json = JSON.stringify(m['script:ld+json'])
- resultMeta.push({
- tag: 'script',
- attrs: {
- type: 'application/ld+json',
- },
- children: escapeHtml(json),
- })
- } catch {
- // Skip invalid JSON-LD objects
- }
- } else {
- const attribute = m.name ?? m.property
- if (attribute) {
- if (metaByAttribute[attribute]) {
- continue
- } else {
- metaByAttribute[attribute] = true
- }
- }
-
- resultMeta.push({
- tag: 'meta',
- attrs: {
- ...m,
- nonce,
- },
- })
- }
- }
- }
-
- if (title) {
- resultMeta.push(title)
- }
-
- if (nonce) {
- resultMeta.push({
- tag: 'meta',
- attrs: {
- property: 'csp-nonce',
- content: nonce,
- },
- })
- }
- resultMeta.reverse()
-
- return resultMeta
- }, [routeMeta, nonce])
-
- // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const links = useStore(
- router.stores.matches,
- (matches) => {
- const constructed = matches
- .flatMap((match) => match.links ?? [])
- .filter((link) => link !== undefined)
- .map((link) => ({
- tag: 'link',
- attrs: {
- ...link,
- nonce,
- },
- })) satisfies Array
-
- return constructed
- },
- deepEqual,
- )
-
- // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const manifestCssTags = useStore(
- router.stores.matches,
- (matches) => {
- const manifest = router.ssr?.manifest
- const tags: Array = []
-
- if (!manifest) {
- return tags
- }
-
- matches.forEach((match) => {
- manifest.routes[match.routeId]?.css?.forEach((link) => {
- const resolvedLink = resolveManifestCssLink(link)
- tags.push({
- tag: 'link',
- attrs: {
- rel: 'stylesheet',
- ...resolvedLink,
- crossOrigin:
- getAssetCrossOrigin(assetCrossOrigin, 'stylesheet') ??
- resolvedLink.crossOrigin,
- suppressHydrationWarning: true,
- nonce,
- },
- })
- })
- })
-
- if (manifest.inlineStyle) {
- tags.push({
- tag: 'style',
- attrs: {
- ...manifest.inlineStyle.attrs,
- nonce,
- },
- children: manifest.inlineStyle.children,
- inlineCss: true,
- })
- }
-
- return tags
- },
- deepEqual,
- )
-
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const preloadLinks = useStore(
- router.stores.matches,
- (matches) => {
- const preloadLinks: Array = []
- const manifest = router.ssr?.manifest
-
- if (!manifest) {
- return preloadLinks
- }
-
- matches.forEach((match) => {
- manifest.routes[match.routeId]?.preloads?.forEach((preload) => {
- preloadLinks.push({
- tag: 'link',
- attrs: {
- ...getScriptPreloadAttrs(manifest, preload, assetCrossOrigin),
- nonce,
- },
- })
- })
- })
-
- return preloadLinks
- },
- deepEqual,
- )
-
- // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const styles = useStore(
- router.stores.matches,
- (matches) => {
- return matches
- .flatMap((match) => match.styles ?? [])
- .filter((style) => style !== undefined)
- .map(({ children, ...attrs }) => ({
- tag: 'style',
- attrs: {
- ...attrs,
- nonce,
- },
- children: children as string | undefined,
- })) satisfies Array
- },
- deepEqual,
- )
-
- // eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const headScripts: Array = useStore(
- router.stores.matches,
- (matches) => {
- return matches
- .flatMap((match) => match.headScripts ?? [])
- .filter((script) => script !== undefined)
- .map(({ children, ...script }) => ({
- tag: 'script',
- attrs: {
- ...script,
- nonce,
- },
- children: children as string | undefined,
- })) satisfies Array
- },
- deepEqual,
- )
-
- const tags: Array = []
- appendUniqueUserTags(tags, meta)
- tags.push(...preloadLinks)
- appendUniqueUserTags(tags, links)
- tags.push(...manifestCssTags)
- appendUniqueUserTags(tags, styles)
- appendUniqueUserTags(tags, headScripts)
- return tags
+ return useStore(router.stores.matches, selectTags, deepEqual)
}
diff --git a/packages/react-router/src/lazyRouteComponent.tsx b/packages/react-router/src/lazyRouteComponent.tsx
index b4fe8703c5..7c387e0e31 100644
--- a/packages/react-router/src/lazyRouteComponent.tsx
+++ b/packages/react-router/src/lazyRouteComponent.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import { isModuleNotFoundError } from '@tanstack/router-core'
+import { isServer } from '@tanstack/router-core/isServer'
import { reactUse } from './utils'
import type { AsyncRouteComponent } from './route'
@@ -24,42 +25,24 @@ export function lazyRouteComponent<
let loadPromise: Promise | undefined
let comp: T[TKey] | T['default']
let error: any
- let reload: boolean
const load = () => {
if (!loadPromise) {
+ error = undefined
loadPromise = importer()
.then((res) => {
- loadPromise = undefined
+ // Keep browser preload behavior unchanged; SSR can reuse the import.
+ if (!(isServer ?? typeof window === 'undefined')) {
+ loadPromise = undefined
+ }
comp = res[exportName ?? 'default']
})
.catch((err) => {
+ loadPromise = undefined
// We don't want an error thrown from preload in this case, because
// there's nothing we want to do about module not found during preload.
// Record the error, the rest is handled during the render path.
error = err
- // If the load fails due to module not found, it may mean a new version of
- // the build was deployed and the user's browser is still using an old version.
- // If this happens, the old version in the user's browser would have an outdated
- // URL to the lazy module.
- // In that case, we want to attempt one window refresh to get the latest.
- if (isModuleNotFoundError(error)) {
- if (
- error instanceof Error &&
- typeof window !== 'undefined' &&
- typeof sessionStorage !== 'undefined'
- ) {
- // Again, we want to reload one time on module not found error and not enter
- // a reload loop if there is some other issue besides an old deploy.
- // That's why we store our reload attempt in sessionStorage.
- // Use error.message as key because it contains the module path that failed.
- const storageKey = `tanstack_router_reload:${error.message}`
- if (!sessionStorage.getItem(storageKey)) {
- sessionStorage.setItem(storageKey, '1')
- reload = true
- }
- }
- }
})
}
@@ -67,15 +50,23 @@ export function lazyRouteComponent<
}
const lazyComp = function Lazy(props: any) {
- // Now that we're out of preload and into actual render path,
- if (reload) {
- // If it was a module loading error,
- // throw eternal suspense while we wait for window to reload
- window.location.reload()
- throw new Promise(() => {})
- }
if (error) {
- // Otherwise, just throw the error
+ // A missing module can mean that a newer deployment replaced the URL.
+ // Reload only for the error that is still current at render time, so a
+ // successful retry cannot leave a stale reload request armed.
+ if (
+ isModuleNotFoundError(error) &&
+ !(isServer ?? typeof window === 'undefined') &&
+ typeof sessionStorage !== 'undefined'
+ ) {
+ const storageKey = `tanstack_router_reload:${error.message}`
+ if (!sessionStorage.getItem(storageKey)) {
+ sessionStorage.setItem(storageKey, '1')
+ window.location.reload()
+ // Suspend forever while the document reloads.
+ throw new Promise(() => {})
+ }
+ }
throw error
}
diff --git a/packages/react-router/src/ssr/RouterClient.tsx b/packages/react-router/src/ssr/RouterClient.tsx
index 3271dcd63f..69abc97f0b 100644
--- a/packages/react-router/src/ssr/RouterClient.tsx
+++ b/packages/react-router/src/ssr/RouterClient.tsx
@@ -3,16 +3,11 @@ import { Await } from '../awaited'
import { RouterProvider } from '../RouterProvider'
import type { AnyRouter } from '@tanstack/router-core'
-let hydrationPromise: Promise>> | undefined
+let hydrationPromise: Promise | undefined
export function RouterClient(props: { router: AnyRouter }) {
- if (!hydrationPromise) {
- if (!props.router.stores.matchesId.get().length) {
- hydrationPromise = hydrate(props.router)
- } else {
- hydrationPromise = Promise.resolve()
- }
- }
+ hydrationPromise ??= hydrate(props.router).finally(() => window.$_TSR!.h())
+
return (
stream.cancel().catch(() => {}) },
+ {
+ signal: request.signal,
+ onAbort: () => stream.cancel().catch(() => {}),
+ },
)
return createSsrStreamResponse(
router,
new Response(responseStream as any, {
- status: router.stores.statusCode.get(),
+ status:
+ router._serverResult?.type === 'render'
+ ? router._serverResult.status
+ : 200,
headers: responseHeaders,
}),
)
@@ -177,7 +183,7 @@ export const renderRouterToStream = async ({
const responseStream = transformPipeableStreamWithRouter(
router,
reactAppPassthrough,
- { onAbort: abortPipeable },
+ { signal: request.signal, onAbort: abortPipeable },
)
responseAttached = true
@@ -199,7 +205,10 @@ export const renderRouterToStream = async ({
return createSsrStreamResponse(
router,
new Response(responseStream as any, {
- status: router.stores.statusCode.get(),
+ status:
+ router._serverResult?.type === 'render'
+ ? router._serverResult.status
+ : 200,
headers: responseHeaders,
}),
)
diff --git a/packages/react-router/src/ssr/renderRouterToString.tsx b/packages/react-router/src/ssr/renderRouterToString.tsx
index e9fe4ec779..5e299cc215 100644
--- a/packages/react-router/src/ssr/renderRouterToString.tsx
+++ b/packages/react-router/src/ssr/renderRouterToString.tsx
@@ -21,7 +21,10 @@ export const renderRouterToString = async ({
}
return new Response(`${html}`, {
- status: router.stores.statusCode.get(),
+ status:
+ router._serverResult?.type === 'render'
+ ? router._serverResult.status
+ : 200,
headers: responseHeaders,
})
} catch (error) {
diff --git a/packages/react-router/src/useMatch.tsx b/packages/react-router/src/useMatch.tsx
index 6c1d03421d..35d6673962 100644
--- a/packages/react-router/src/useMatch.tsx
+++ b/packages/react-router/src/useMatch.tsx
@@ -20,12 +20,7 @@ import type {
ThrowOrOptional,
} from '@tanstack/router-core'
-const dummyStore = {
- get() {},
- subscribe() {
- return { unsubscribe() {} }
- },
-} as any
+const dummyMatch = {}
export function useStructuralSharing<
TRouter extends AnyRouter,
@@ -147,16 +142,15 @@ export function useMatch<
>,
): ThrowOrOptional, TThrow> {
const router = useRouter()
- const nearestMatchId = React.useContext(
+ const nearestRouteId = React.useContext(
opts.from ? dummyMatchContext : matchContext,
)
- const matchStore = opts.from
- ? router.stores.getRouteMatchStore(opts.from)
- : router.stores.matchStores.get(nearestMatchId!)
+ const routeId = opts.from ?? nearestRouteId
+ const matchStore = router.stores.getMatchStore(routeId!)
if (isServer ?? router.isServer) {
- const match = matchStore?.get()
+ const match = matchStore.get()
if (!match) {
if (opts.shouldThrow ?? true) {
if (process.env.NODE_ENV !== 'production') {
@@ -179,12 +173,12 @@ export function useMatch<
useStructuralSharing(opts, router)
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
- const matchSelection = useStore(matchStore ?? dummyStore, (match) =>
- match ? selector(match as any) : dummyStore,
+ const matchSelection = useStore(matchStore, (match) =>
+ match ? selector(match as any) : dummyMatch,
)
- if (matchSelection !== dummyStore) {
- return matchSelection
+ if (matchSelection !== dummyMatch) {
+ return matchSelection as any
}
if (opts.shouldThrow ?? true) {
diff --git a/packages/react-router/src/utils.ts b/packages/react-router/src/utils.ts
index f3cbf54613..ca0653e350 100644
--- a/packages/react-router/src/utils.ts
+++ b/packages/react-router/src/utils.ts
@@ -1,5 +1,6 @@
'use client'
import * as React from 'react'
+import { isServer } from '@tanstack/router-core/isServer'
// Safe version of React.use() that will not cause compilation errors against
// React 18 with Webpack, which statically analyzes imports and fails when it
@@ -27,7 +28,9 @@ export function useStableCallback) => any>(
}
export const useLayoutEffect =
- typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
+ (isServer ?? typeof window === 'undefined')
+ ? React.useEffect
+ : React.useLayoutEffect
/**
* Taken from https://www.developerway.com/posts/implementing-advanced-use-previous-hook#part3
diff --git a/packages/react-router/tests/Matches.test.tsx b/packages/react-router/tests/Matches.test.tsx
index dd3ca3dfa6..33153a4e11 100644
--- a/packages/react-router/tests/Matches.test.tsx
+++ b/packages/react-router/tests/Matches.test.tsx
@@ -1,6 +1,14 @@
import { afterEach, describe, expect, test } from 'vitest'
-import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
+import {
+ act,
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
import { createMemoryHistory } from '@tanstack/history'
+import { createControlledPromise } from '@tanstack/router-core'
import {
Link,
Outlet,
@@ -126,11 +134,11 @@ test('when filtering useMatches by loaderData', async () => {
test('should show pendingComponent of root route', async () => {
const root = createRootRoute({
- pendingComponent: () =>
,
+ pendingComponent: () =>
,
loader: async () => {
await new Promise((r) => setTimeout(r, 50))
},
- component: () =>
,
+ component: () =>
,
})
const router = createRouter({
@@ -145,6 +153,134 @@ test('should show pendingComponent of root route', async () => {
expect(await rendered.findByTestId('root-content')).toBeInTheDocument()
})
+test('useMatchRoute follows superseding pending locations', async () => {
+ const aGate = createControlledPromise()
+ const bGate = createControlledPromise()
+
+ function Layout() {
+ const matchRoute = useMatchRoute()
+ return (
+
+
+ {String(Boolean(matchRoute({ to: '/a', pending: true })))}
+
+
+ {String(Boolean(matchRoute({ to: '/b', pending: true })))}
+
+
+ {String(Boolean(matchRoute({ to: '/b', pending: false })))}
+
+
+
+ )
+ }
+
+ const root = createRootRoute({ component: Layout })
+ const index = createRoute({
+ getParentRoute: () => root,
+ path: '/',
+ })
+ const a = createRoute({
+ getParentRoute: () => root,
+ path: '/a',
+ loader: () => aGate,
+ })
+ const b = createRoute({
+ getParentRoute: () => root,
+ path: '/b',
+ loader: () => bGate,
+ })
+ const router = createRouter({
+ routeTree: root.addChildren([index, a, b]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+ render( )
+
+ await waitFor(() => {
+ expect(screen.getByTestId('pending-a')).toHaveTextContent('false')
+ expect(router.stores.status.get()).toBe('idle')
+ expect(router.stores.resolvedLocation.get()?.pathname).toBe('/')
+ })
+
+ let navigationA!: Promise
+ act(() => {
+ navigationA = router.navigate({ to: '/a' })
+ })
+ await waitFor(() => {
+ expect(screen.getByTestId('pending-a')).toHaveTextContent('true')
+ expect(screen.getByTestId('resolved-b')).toHaveTextContent('false')
+ })
+
+ let navigationB!: Promise
+ act(() => {
+ navigationB = router.navigate({ to: '/b' })
+ })
+ await waitFor(() => {
+ expect(screen.getByTestId('pending-a')).toHaveTextContent('false')
+ expect(screen.getByTestId('pending-b')).toHaveTextContent('true')
+ expect(screen.getByTestId('resolved-b')).toHaveTextContent('false')
+ })
+
+ await act(async () => {
+ aGate.resolve()
+ bGate.resolve()
+ await Promise.allSettled([navigationA, navigationB])
+ })
+ await waitFor(() => {
+ expect(screen.getByTestId('pending-b')).toHaveTextContent('false')
+ expect(screen.getByTestId('resolved-b')).toHaveTextContent('true')
+ })
+})
+
+test('legacy notFoundRoute drops a stale parent layout after navigation', async () => {
+ let legacyLoads = 0
+ const root = createRootRoute({ component: Outlet })
+ const parent = createRoute({
+ getParentRoute: () => root,
+ path: '/parent',
+ component: () => (
+
+ Parent layout
+
+
+ ),
+ })
+ const known = createRoute({
+ getParentRoute: () => parent,
+ path: '/known',
+ })
+ const legacyNotFound = createRoute({
+ getParentRoute: () => root,
+ path: '/404',
+ loader: () => {
+ legacyLoads++
+ return 'not found'
+ },
+ component: () => Legacy not found
,
+ staleTime: Infinity,
+ gcTime: Infinity,
+ })
+ const router = createRouter({
+ routeTree: root.addChildren([parent.addChildren([known])]),
+ history: createMemoryHistory({ initialEntries: ['/parent/missing'] }),
+ notFoundRoute: legacyNotFound,
+ })
+
+ const rendered = render( )
+ expect(await rendered.findByText('Parent layout')).toBeInTheDocument()
+ expect(await rendered.findByText('Legacy not found')).toBeInTheDocument()
+ expect(legacyLoads).toBe(1)
+
+ await act(async () => {
+ await router.navigate({ to: '/missing' } as any)
+ })
+
+ expect(rendered.queryByText('Parent layout')).not.toBeInTheDocument()
+ expect(await rendered.findByText('Legacy not found')).toBeInTheDocument()
+ expect(legacyLoads).toBe(1)
+ rendered.unmount()
+})
+
describe('matching on different param types', () => {
const testCases = [
{
diff --git a/packages/react-router/tests/Scripts.test.tsx b/packages/react-router/tests/Scripts.test.tsx
index b7605c9402..893cef01bc 100644
--- a/packages/react-router/tests/Scripts.test.tsx
+++ b/packages/react-router/tests/Scripts.test.tsx
@@ -1,4 +1,4 @@
-import { afterEach, describe, expect, test } from 'vitest'
+import { afterEach, describe, expect, test, vi } from 'vitest'
import {
act,
cleanup,
@@ -9,11 +9,14 @@ import {
} from '@testing-library/react'
import { createPortal } from 'react-dom'
import ReactDOMServer from 'react-dom/server'
+import { hydrate } from '@tanstack/router-core/ssr/client'
+import { dehydrateSsrMatchId } from '../../router-core/src/ssr/ssr-match-id'
import {
HeadContent,
Link,
Outlet,
+ RouterContextProvider,
RouterProvider,
createBrowserHistory,
createMemoryHistory,
@@ -57,6 +60,7 @@ afterEach(() => {
cleanup()
browserHistories.splice(0).forEach((history) => history.destroy())
window.history.replaceState(null, 'root', '/')
+ delete window.$_TSR
})
describe('ssr scripts', () => {
@@ -158,15 +162,12 @@ describe('ssr scripts', () => {
{ src: 'script3.js' },
])
- const { container } = await act(() =>
- render( ),
- )
- expect(await screen.findByTestId('root')).toBeInTheDocument()
- expect(await screen.findByTestId('index')).toBeInTheDocument()
-
- expect(container.innerHTML).toEqual(
- ``,
+ const html = ReactDOMServer.renderToString(
+ ,
)
+ expect(html).toContain('')
+ expect(html).toContain('')
+ expect(html).not.toContain('script2.js')
})
})
@@ -337,6 +338,119 @@ describe('scripts with async/defer attributes', () => {
})
describe('ssr HeadContent', () => {
+ test('renders descendant assets during a data-only hydration handoff', async () => {
+ const rootRoute = createRootRoute({})
+ const dataOnlyRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/report',
+ ssr: 'data-only',
+ loader: () => 'report',
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => dataOnlyRoute,
+ path: '/details',
+ loader: () => 'details',
+ head: () => ({
+ meta: [{ name: 'data-only-child', content: 'visible' }],
+ links: [{ rel: 'preload', href: '/data-only-head-link.js' }],
+ styles: [
+ {
+ id: 'data-only-route-style',
+ children: '.data-only-child { color: green }',
+ },
+ ],
+ scripts: [
+ {
+ id: 'data-only-head-script',
+ type: 'application/json',
+ children: '{"source":"head"}',
+ },
+ ],
+ }),
+ scripts: () => [
+ {
+ id: 'data-only-body-script',
+ type: 'application/json',
+ children: '{"source":"body"}',
+ },
+ ],
+ })
+ const router = createRouter({
+ history: createMemoryHistory({
+ initialEntries: ['/report/details'],
+ }),
+ routeTree: rootRoute.addChildren([
+ dataOnlyRoute.addChildren([childRoute]),
+ ]),
+ })
+ const matches = router.matchRoutes(router.latestLocation)
+ window.$_TSR = {
+ router: {
+ dehydratedData: {},
+ manifest: {
+ routes: {
+ [childRoute.id]: {
+ css: ['/data-only-manifest.css'],
+ preloads: ['/data-only-manifest.js'],
+ scripts: [
+ {
+ attrs: {
+ id: 'data-only-manifest-script',
+ type: 'application/json',
+ },
+ children: '{"source":"manifest"}',
+ },
+ ],
+ },
+ },
+ },
+ matches: matches.map((match, index) => ({
+ i: dehydrateSsrMatchId(match.id),
+ s: 'success',
+ ssr: index === 1 ? 'data-only' : true,
+ l: index ? (index === 1 ? 'report' : 'details') : undefined,
+ u: Date.now(),
+ })),
+ },
+ h: vi.fn(),
+ e: vi.fn(),
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ }
+
+ await hydrate(router)
+
+ expect(router.state.matches.map((match) => match.status)).toEqual([
+ 'success',
+ 'pending',
+ 'success',
+ ])
+ render(
+
+
+
+ ,
+ )
+
+ expect(
+ document.querySelector('meta[name="data-only-child"]'),
+ ).not.toBeNull()
+ expect(
+ document.querySelector('link[href="/data-only-head-link.js"]'),
+ ).not.toBeNull()
+ expect(document.querySelector('#data-only-route-style')).not.toBeNull()
+ expect(document.querySelector('#data-only-head-script')).not.toBeNull()
+ expect(document.querySelector('#data-only-body-script')).not.toBeNull()
+ expect(
+ document.querySelector('link[href="/data-only-manifest.css"]'),
+ ).not.toBeNull()
+ expect(
+ document.querySelector('link[href="/data-only-manifest.js"]'),
+ ).not.toBeNull()
+ expect(document.querySelector('#data-only-manifest-script')).not.toBeNull()
+ })
+
test('derives title, dedupes meta, and allows non-loader HeadContent', async () => {
const rootRoute = createRootRoute({
loader: () =>
@@ -839,6 +953,130 @@ describe('ssr HeadContent', () => {
),
).toHaveLength(1)
})
+
+ test('does not render retained descendant assets past a terminal parent boundary', async () => {
+ let failParent = false
+ const childHeadScript = '{"source":"child-head"}'
+ const childBodyScript = '{"source":"child-body"}'
+ const childManifestScript = '{"source":"child-manifest"}'
+ const childPreload = '/terminal-child-preload.js'
+ const childHeadLink = '/terminal-child-head-link.js'
+ const rootRoute = createRootRoute({
+ component: () => (
+ <>
+ {createPortal( , document.head)}
+
+
+ >
+ ),
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ shouldReload: true,
+ loader: () => {
+ if (failParent) {
+ throw new Error('parent failed')
+ }
+ },
+ component: Outlet,
+ errorComponent: () => Parent error
,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ head: () => ({
+ meta: [{ name: 'terminal-child', content: 'visible' }],
+ links: [{ rel: 'preload', href: childHeadLink }],
+ styles: [{ children: '.terminal-child { color: red }' }],
+ scripts: [{ type: 'application/ld+json', children: childHeadScript }],
+ }),
+ scripts: () => [
+ { type: 'application/ld+json', children: childBodyScript },
+ ],
+ component: () => Child content
,
+ })
+ const router = createRouter({
+ history: createMemoryHistory({ initialEntries: ['/parent/child'] }),
+ routeTree: rootRoute.addChildren([parentRoute.addChildren([childRoute])]),
+ })
+ router.ssr = {
+ manifest: {
+ routes: {
+ [childRoute.id]: {
+ preloads: [childPreload],
+ scripts: [
+ {
+ attrs: { type: 'application/ld+json' },
+ children: childManifestScript,
+ },
+ ],
+ },
+ },
+ },
+ }
+
+ await router.load()
+ await act(() => render( ))
+
+ await waitFor(() => {
+ expect(
+ document.head.querySelector('meta[name="terminal-child"]'),
+ ).not.toBeNull()
+ expect(
+ document.head.querySelector(`link[href="${childPreload}"]`),
+ ).not.toBeNull()
+ expect(
+ document.head.querySelector(`link[href="${childHeadLink}"]`),
+ ).not.toBeNull()
+ expect(document.head.textContent).toContain(
+ '.terminal-child { color: red }',
+ )
+ expect(document.documentElement.textContent).toContain(childHeadScript)
+ expect(document.documentElement.textContent).toContain(childBodyScript)
+ expect(document.documentElement.textContent).toContain(
+ childManifestScript,
+ )
+ })
+
+ failParent = true
+ await act(() => router.invalidate())
+ await screen.findByText('Parent error')
+
+ expect(router.state.matches).toHaveLength(3)
+ expect(router.state.matches[1]).toMatchObject({
+ routeId: parentRoute.id,
+ status: 'error',
+ })
+ expect(router.state.matches[2]).toMatchObject({
+ routeId: childRoute.id,
+ meta: [{ name: 'terminal-child', content: 'visible' }],
+ scripts: [{ type: 'application/ld+json', children: childBodyScript }],
+ })
+ await waitFor(() => {
+ expect(
+ document.head.querySelector('meta[name="terminal-child"]'),
+ ).toBeNull()
+ expect(
+ document.head.querySelector(`link[href="${childPreload}"]`),
+ ).toBeNull()
+ expect(
+ document.head.querySelector(`link[href="${childHeadLink}"]`),
+ ).toBeNull()
+ expect(document.head.textContent).not.toContain(
+ '.terminal-child { color: red }',
+ )
+ expect(document.documentElement.textContent).not.toContain(
+ childHeadScript,
+ )
+ expect(document.documentElement.textContent).not.toContain(
+ childBodyScript,
+ )
+ expect(document.documentElement.textContent).not.toContain(
+ childManifestScript,
+ )
+ })
+ })
})
describe('data script rendering', () => {
diff --git a/packages/react-router/tests/ancestor-loader-child-pending-min.test.tsx b/packages/react-router/tests/ancestor-loader-child-pending-min.test.tsx
new file mode 100644
index 0000000000..09945bb9af
--- /dev/null
+++ b/packages/react-router/tests/ancestor-loader-child-pending-min.test.tsx
@@ -0,0 +1,157 @@
+import * as React from 'react'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
+
+test('a child fallback revealed after a fresh ancestor loader keeps its own pendingMinMs', async () => {
+ vi.useFakeTimers()
+
+ const parentLoader = createControlledPromise()
+ const childLoader = createControlledPromise()
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ loader: () => parentLoader,
+ component: () => ,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => Child pending
,
+ loader: () => childLoader,
+ component: () => Child content
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ parentRoute.addChildren([childRoute]),
+ ]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ defaultPendingMs: 0,
+ })
+
+ await router.load()
+ render( )
+ expect(screen.getByText('Index')).toBeInTheDocument()
+
+ let navigation!: Promise
+ await act(async () => {
+ navigation = router.navigate({ to: '/parent/child' })
+ await vi.advanceTimersByTimeAsync(25)
+ })
+
+ expect(screen.queryByText('Child pending')).not.toBeInTheDocument()
+
+ await act(async () => {
+ parentLoader.resolve()
+ await vi.advanceTimersByTimeAsync(0)
+ })
+ expect(screen.getByText('Child pending')).toBeInTheDocument()
+ expect(screen.queryByText('Child content')).not.toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(25)
+ childLoader.resolve()
+ await Promise.resolve()
+ })
+
+ // The minimum is measured from the child's first visible frame, not from
+ // the navigation start or the ancestor's completion.
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(74)
+ })
+ expect(screen.getByText('Child pending')).toBeInTheDocument()
+ expect(screen.queryByText('Child content')).not.toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ await navigation
+ })
+ expect(screen.queryByText('Child pending')).not.toBeInTheDocument()
+ expect(screen.getByText('Child content')).toBeInTheDocument()
+})
+
+test('advancing from a parent loader to a parallel child loader does not restart pendingMs', async () => {
+ vi.useFakeTimers()
+
+ const parentLoader = createControlledPromise()
+ const childLoader = createControlledPromise()
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ loader: () => parentLoader,
+ component: Outlet,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ pendingMs: 1_000,
+ pendingMinMs: 0,
+ pendingComponent: () => Child pending
,
+ loader: () => childLoader,
+ component: () => Child content
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ parentRoute.addChildren([childRoute]),
+ ]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ await router.load()
+ render( )
+
+ let navigation!: Promise
+ await act(async () => {
+ navigation = router.navigate({ to: '/parent/child' })
+ await vi.advanceTimersByTimeAsync(900)
+ })
+ expect(screen.queryByText('Child pending')).not.toBeInTheDocument()
+
+ await act(async () => {
+ parentLoader.resolve()
+ await vi.advanceTimersByTimeAsync(99)
+ })
+ expect(screen.queryByText('Child pending')).not.toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ })
+ expect(screen.getByText('Child pending')).toBeInTheDocument()
+
+ await act(async () => {
+ childLoader.resolve()
+ await navigation
+ })
+ expect(screen.getByText('Child content')).toBeInTheDocument()
+})
diff --git a/packages/react-router/tests/component-preload-retry-pending-min.test.tsx b/packages/react-router/tests/component-preload-retry-pending-min.test.tsx
new file mode 100644
index 0000000000..dcdd94af67
--- /dev/null
+++ b/packages/react-router/tests/component-preload-retry-pending-min.test.tsx
@@ -0,0 +1,150 @@
+import * as React from 'react'
+import { act } from 'react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { createControlledPromise } from '@tanstack/router-core'
+import {
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ useRouter,
+} from '../src'
+import type { ErrorComponentProps } from '../src'
+
+afterEach(() => {
+ vi.useRealTimers()
+ vi.restoreAllMocks()
+ cleanup()
+})
+
+test('delayed component preload reveals pending UI', async () => {
+ const componentGate = createControlledPromise()
+ const Page = Object.assign(() => Page content
, {
+ preload: () => componentGate,
+ })
+ const rootRoute = createRootRoute({})
+ const pageRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ component: Page,
+ pendingMs: 10,
+ pendingComponent: () => Loading page...
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/page'] }),
+ })
+
+ render( )
+
+ expect(await screen.findByText('Loading page...')).toBeInTheDocument()
+ componentGate.resolve()
+ expect(await screen.findByText('Page content')).toBeInTheDocument()
+ expect(screen.queryByText('Loading page...')).not.toBeInTheDocument()
+})
+
+/**
+ * A component-only route can fail while preloading its code, then retry from
+ * its error UI through invalidate(). The retry is a fresh pending generation:
+ * its fallback must remain visible until the retried component preload is
+ * ready and pendingMinMs has elapsed.
+ */
+test('component preload retry remains pending through pendingMinMs', async () => {
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ const retryChunk = createControlledPromise()
+ let preloadAttempt = 0
+ let retryInvalidation!: Promise
+ let retrySettled = false
+
+ const Page = Object.assign(
+ () => Page content
,
+ {
+ preload: vi.fn(() => {
+ preloadAttempt++
+ return preloadAttempt === 1
+ ? Promise.reject(new Error('initial chunk request failed'))
+ : retryChunk
+ }),
+ },
+ )
+
+ function RetryError({ reset }: ErrorComponentProps) {
+ const router = useRouter()
+ return (
+ {
+ reset()
+ retryInvalidation = router.invalidate()
+ void retryInvalidation.then(() => {
+ retrySettled = true
+ })
+ }}
+ >
+ Retry chunk
+
+ )
+ }
+
+ const rootRoute = createRootRoute({})
+ const pageRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ component: Page,
+ errorComponent: RetryError,
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => (
+ Loading page...
+ ),
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/page'] }),
+ })
+
+ render( )
+
+ expect(
+ await screen.findByRole('button', { name: 'Retry chunk' }),
+ ).toBeInTheDocument()
+ expect(Page.preload).toHaveBeenCalledTimes(1)
+
+ vi.useFakeTimers()
+ fireEvent.click(screen.getByRole('button', { name: 'Retry chunk' }))
+
+ // Let pendingMs: 0 publish the retry lane.
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(0)
+ })
+ expect(Page.preload).toHaveBeenCalledTimes(2)
+ expect(screen.getByTestId('page-pending')).toBeInTheDocument()
+ expect(
+ screen.queryByRole('button', { name: 'Retry chunk' }),
+ ).not.toBeInTheDocument()
+
+ await act(async () => {
+ retryChunk.resolve()
+ await Promise.resolve()
+ })
+
+ expect.soft(retrySettled).toBe(false)
+ expect.soft(screen.queryByTestId('page-pending')).toBeInTheDocument()
+ expect.soft(screen.queryByTestId('page-content')).not.toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(99)
+ })
+ expect(screen.getByTestId('page-pending')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ await retryInvalidation
+ })
+
+ expect(screen.getByTestId('page-content')).toBeInTheDocument()
+ expect(screen.queryByTestId('page-pending')).not.toBeInTheDocument()
+})
diff --git a/packages/react-router/tests/component-preload-retry.test.tsx b/packages/react-router/tests/component-preload-retry.test.tsx
new file mode 100644
index 0000000000..ff33ec4bea
--- /dev/null
+++ b/packages/react-router/tests/component-preload-retry.test.tsx
@@ -0,0 +1,128 @@
+import * as React from 'react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { createControlledPromise } from '@tanstack/router-core'
+import {
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ lazyRouteComponent,
+ useRouter,
+} from '../src'
+import type { ErrorComponentProps } from '../src'
+
+afterEach(() => {
+ cleanup()
+ vi.restoreAllMocks()
+ vi.unstubAllGlobals()
+ sessionStorage.clear()
+})
+
+test('a successful server component download is reused', async () => {
+ vi.stubGlobal('window', undefined)
+ const importer = vi.fn().mockResolvedValue({ default: () => null })
+ const Page = lazyRouteComponent(importer)
+
+ await Page.preload?.()
+ await Page.preload?.()
+ expect(importer).toHaveBeenCalledTimes(1)
+})
+
+test('concurrent component preloads share the import', async () => {
+ const componentImport = createControlledPromise<{
+ default: () => null
+ }>()
+ const importer = vi.fn(() => componentImport)
+ const Page = lazyRouteComponent(importer)
+
+ const first = Page.preload?.()
+ const second = Page.preload?.()
+ expect(importer).toHaveBeenCalledOnce()
+
+ componentImport.resolve({ default: () => null })
+ await Promise.all([first, second])
+})
+
+test('a failed component download is retried from the route error UI', async () => {
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ const PageContent = () => Page content
+ const importer = vi
+ .fn<() => Promise<{ default: typeof PageContent }>>()
+ .mockRejectedValueOnce(new Error('component download failed'))
+ .mockResolvedValue({ default: PageContent })
+ const Page = lazyRouteComponent(importer)
+
+ function RouteError({ reset }: ErrorComponentProps) {
+ const router = useRouter()
+ return (
+ {
+ reset()
+ void router.invalidate()
+ }}
+ >
+ Retry
+
+ )
+ }
+
+ const rootRoute = createRootRoute()
+ const pageRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ component: Page,
+ errorComponent: RouteError,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/page'] }),
+ })
+
+ render( )
+
+ const retryButton = await screen.findByRole('button', { name: 'Retry' })
+ expect(importer).toHaveBeenCalledTimes(1)
+
+ fireEvent.click(retryButton)
+
+ expect(await screen.findByText('Page content')).toBeInTheDocument()
+ expect(importer).toHaveBeenCalledTimes(2)
+ expect(
+ screen.queryByRole('button', { name: 'Retry' }),
+ ).not.toBeInTheDocument()
+})
+
+test('renders after retrying a module download that failed during preload', async () => {
+ const PageContent = () => Page content
+ const importer = vi
+ .fn<() => Promise<{ default: typeof PageContent }>>()
+ .mockRejectedValueOnce(
+ new TypeError(
+ 'Failed to fetch dynamically imported module: /assets/page.js',
+ ),
+ )
+ .mockResolvedValue({ default: PageContent })
+ const Page = lazyRouteComponent(importer)
+
+ await Page.preload?.()
+
+ const rootRoute = createRootRoute()
+ const pageRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ component: Page,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/page'] }),
+ })
+
+ render( )
+
+ expect(await screen.findByText('Page content')).toBeInTheDocument()
+ expect(importer).toHaveBeenCalledTimes(2)
+})
diff --git a/packages/react-router/tests/errorComponent.test.tsx b/packages/react-router/tests/errorComponent.test.tsx
index 0779c2e8c4..b5ee4b6709 100644
--- a/packages/react-router/tests/errorComponent.test.tsx
+++ b/packages/react-router/tests/errorComponent.test.tsx
@@ -1,12 +1,13 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
-import ReactDOMServer from 'react-dom/server'
import {
+ HeadContent,
Link,
Outlet,
RouterProvider,
createBrowserHistory,
+ createControlledPromise,
createLazyRoute,
createMemoryHistory,
createRootRoute,
@@ -14,6 +15,11 @@ import {
createRouter,
notFound,
} from '../src'
+import {
+ RouterServer,
+ createRequestHandler,
+ renderRouterToString,
+} from '../src/ssr/server'
import type { ErrorComponentProps, RouterHistory } from '../src'
function MyErrorComponent(props: ErrorComponentProps) {
@@ -267,6 +273,109 @@ describe.each([true, false])(
},
)
+test('global catch boundary resets when a background child generation recovers', async () => {
+ const refresh = createControlledPromise()
+ let loaderCalls = 0
+ const rootRoute = createRootRoute({ component: Outlet })
+ const childRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: {
+ staleReloadMode: 'background',
+ handler: () => (++loaderCalls === 1 ? 1 : refresh),
+ },
+ component: () => {
+ const revision = childRoute.useLoaderData()
+ if (revision === 1) {
+ throw new Error('stale child render failed')
+ }
+ return Recovered child revision {revision}
+ },
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([childRoute]),
+ history,
+ })
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ render( )
+ expect(
+ await screen.findByText('stale child render failed'),
+ ).toBeInTheDocument()
+
+ const invalidation = router.invalidate()
+ await vi.waitFor(() => expect(loaderCalls).toBe(2))
+ expect(screen.getByText('stale child render failed')).toBeInTheDocument()
+ expect(screen.queryByText(/Recovered child revision/)).not.toBeInTheDocument()
+ refresh.resolve(2)
+ await invalidation
+
+ expect(
+ await screen.findByText('Recovered child revision 2'),
+ ).toBeInTheDocument()
+ expect(
+ screen.queryByText('stale child render failed'),
+ ).not.toBeInTheDocument()
+})
+
+test('ancestor route errorComponent resets when a background child generation recovers', async () => {
+ const refresh = createControlledPromise()
+ let loaderCalls = 0
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ errorComponent: ({ error }) => Ancestor error: {error.message}
,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: {
+ staleReloadMode: 'background',
+ handler: () => (++loaderCalls === 1 ? 1 : refresh),
+ },
+ component: () => {
+ const revision = childRoute.useLoaderData()
+ if (revision === 1) {
+ throw new Error('stale child render failed')
+ }
+ return Recovered child revision {revision}
+ },
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([childRoute]),
+ history,
+ })
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ let invalidation: Promise | undefined
+ try {
+ render( )
+ expect(
+ await screen.findByText('Ancestor error: stale child render failed'),
+ ).toBeInTheDocument()
+
+ invalidation = router.invalidate({
+ filter: (match) => match.routeId === childRoute.id,
+ })
+ await vi.waitFor(() => expect(loaderCalls).toBe(2))
+ expect(
+ screen.getByText('Ancestor error: stale child render failed'),
+ ).toBeInTheDocument()
+ refresh.resolve(2)
+ await invalidation
+
+ expect(
+ await screen.findByText('Recovered child revision 2'),
+ ).toBeInTheDocument()
+ } finally {
+ refresh.resolve(2)
+ if (invalidation) {
+ await Promise.allSettled([invalidation])
+ }
+ }
+})
+
test('errorComponent receives primitive errors thrown from beforeLoad', async () => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
@@ -313,8 +422,47 @@ test('errorComponent receives primitive errors thrown from beforeLoad', async ()
expect(screen.queryByText('About route content')).not.toBeInTheDocument()
})
+test.each(['beforeLoad', 'loader'] as const)(
+ 'a Promise synchronously thrown from %s renders the route error UI',
+ async (hook) => {
+ const thrown = Promise.resolve('not route data')
+ const rootRoute = createRootRoute()
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ beforeLoad:
+ hook === 'beforeLoad'
+ ? () => {
+ throw thrown
+ }
+ : undefined,
+ loader:
+ hook === 'loader'
+ ? () => {
+ throw thrown
+ }
+ : undefined,
+ errorComponent: ({ error }) => (
+
+ {error instanceof Error && error.cause === thrown
+ ? 'Promise route error'
+ : 'Wrong route error'}
+
+ ),
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+
+ expect(await screen.findByText('Promise route error')).toBeInTheDocument()
+ expect(screen.queryByText('Wrong route error')).not.toBeInTheDocument()
+ },
+)
+
test('SSR errorComponent receives primitive errors thrown from beforeLoad', async () => {
- const history = createMemoryHistory({ initialEntries: ['/about'] })
const rootRoute = createRootRoute({
component: function Root() {
return
@@ -330,20 +478,301 @@ test('SSR errorComponent receives primitive errors thrown from beforeLoad', asyn
errorComponent: ({ error }) => Error: {String(error)}
,
})
- const router = createRouter({
- routeTree: rootRoute.addChildren([aboutRoute]),
- history,
+ const handler = createRequestHandler({
+ request: new Request('http://localhost/about'),
+ createRouter: () =>
+ createRouter({
+ routeTree: rootRoute.addChildren([aboutRoute]),
+ isServer: true,
+ }),
})
- router.isServer = true
- await router.load()
+ const response = await handler(({ router, responseHeaders }) =>
+ renderRouterToString({
+ router,
+ responseHeaders,
+ children: ,
+ }),
+ )
- expect(router.state.statusCode).toBe(500)
- const html = ReactDOMServer.renderToString( )
+ expect(response.status).toBe(500)
+ const html = await response.text()
expect(html).toContain('Error:')
expect(html).toContain('primitive error thrown')
})
+test('a later fresh ancestor loader failure owns the reachable boundary', async () => {
+ const parentStarted = createControlledPromise()
+ const childStarted = createControlledPromise()
+ const parentGate = createControlledPromise()
+ const childGate = createControlledPromise()
+ const childSettled = createControlledPromise()
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ component: Outlet,
+ loader: async () => {
+ parentStarted.resolve()
+ await parentGate
+ throw new Error('later parent failure')
+ },
+ errorComponent: () => Parent error boundary
,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ loader: async () => {
+ childStarted.resolve()
+ await childGate
+ childSettled.resolve()
+ throw new Error('first child failure')
+ },
+ errorComponent: () => Child error boundary
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ parentRoute.addChildren([childRoute]),
+ ]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ await router.load()
+ render( )
+
+ let navigation!: Promise
+ await act(async () => {
+ navigation = router.navigate({ to: '/parent/child' })
+ await Promise.all([parentStarted, childStarted])
+ })
+ await act(async () => {
+ childGate.resolve()
+ await childSettled
+ parentGate.resolve()
+ await navigation
+ })
+
+ expect(screen.getByText('Parent error boundary')).toBeInTheDocument()
+ expect(screen.queryByText('Child error boundary')).not.toBeInTheDocument()
+})
+
+test('a fresh ancestor failure waits for lazy options before rendering its error', async () => {
+ const parentStarted = createControlledPromise()
+ const childStarted = createControlledPromise()
+ const parentGate = createControlledPromise()
+ const childGate = createControlledPromise()
+ const childSettled = createControlledPromise()
+ const lazyParentOptions = createLazyRoute('/parent')({
+ component: () => (
+ <>
+ Lazy parent shell
+
+ >
+ ),
+ })
+ const parentChunk = createControlledPromise()
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ loader: async () => {
+ parentStarted.resolve()
+ await parentGate
+ throw new Error('later parent failure')
+ },
+ errorComponent: () => Parent error boundary
,
+ }).lazy(() => parentChunk)
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ loader: async () => {
+ childStarted.resolve()
+ await childGate
+ childSettled.resolve()
+ throw new Error('first child failure')
+ },
+ errorComponent: () => Child error boundary
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ parentRoute.addChildren([childRoute]),
+ ]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ await router.load()
+ render( )
+
+ let navigation: Promise | undefined
+ try {
+ await act(async () => {
+ navigation = router.navigate({ to: '/parent/child' })
+ await Promise.all([parentStarted, childStarted])
+ })
+ await act(async () => {
+ childGate.resolve()
+ await childSettled
+ parentGate.resolve()
+ await Promise.resolve()
+ })
+
+ expect(parentChunk.status).toBe('pending')
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.queryByText('Parent error boundary')).not.toBeInTheDocument()
+ expect(screen.queryByText('Child error boundary')).not.toBeInTheDocument()
+ expect(screen.queryByText('Lazy parent shell')).not.toBeInTheDocument()
+ } finally {
+ await act(async () => {
+ childGate.resolve()
+ parentGate.resolve()
+ parentChunk.resolve(lazyParentOptions)
+ await navigation
+ })
+ }
+
+ expect(screen.getByText('Parent error boundary')).toBeInTheDocument()
+ expect(screen.queryByText('Lazy parent shell')).not.toBeInTheDocument()
+ expect(screen.queryByText('Child error boundary')).not.toBeInTheDocument()
+})
+
+test('SSR renders a later fresh ancestor loader failure', async () => {
+ const parentStarted = createControlledPromise()
+ const childStarted = createControlledPromise()
+ const parentGate = createControlledPromise()
+ const childGate = createControlledPromise()
+ const childSettled = createControlledPromise()
+ const rootRoute = createRootRoute({ component: Outlet })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ component: Outlet,
+ loader: async () => {
+ parentStarted.resolve()
+ await parentGate
+ throw new Error('later parent failure')
+ },
+ errorComponent: () => Parent error boundary
,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ loader: async () => {
+ childStarted.resolve()
+ await childGate
+ childSettled.resolve()
+ throw new Error('first child failure')
+ },
+ errorComponent: () => Child error boundary
,
+ })
+ const handler = createRequestHandler({
+ request: new Request('http://localhost/parent/child'),
+ createRouter: () =>
+ createRouter({
+ routeTree: rootRoute.addChildren([
+ parentRoute.addChildren([childRoute]),
+ ]),
+ isServer: true,
+ }),
+ })
+
+ const responsePromise = handler(({ router, responseHeaders }) =>
+ renderRouterToString({
+ router,
+ responseHeaders,
+ children: ,
+ }),
+ )
+ await Promise.all([parentStarted, childStarted])
+ childGate.resolve()
+ await childSettled
+ parentGate.resolve()
+ const response = await responsePromise
+ const html = await response.text()
+
+ expect(response.status).toBe(500)
+ expect(html).toContain('Parent error boundary')
+ expect(html).not.toContain('Child error boundary')
+})
+
+// https://github.com/TanStack/router/issues/4684
+test('#4684: SSR renders head content when beforeLoad throws', async () => {
+ const rootRoute = createRootRoute({
+ head: () => ({
+ links: [{ rel: 'stylesheet', href: '/global.css' }],
+ }),
+ shellComponent: function RootDocument({ children }) {
+ return (
+
+
+
+
+ {children}
+
+ )
+ },
+ component: Outlet,
+ })
+ const failingRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/fail',
+ beforeLoad: () => {
+ throw new Error('beforeLoad failed')
+ },
+ head: ({ match }) => ({
+ meta: [{ title: match.error ? 'Error title' : 'Success title' }],
+ }),
+ component: function FailingRoute() {
+ return Route content
+ },
+ errorComponent: ({ error }) => Error UI: {error.message}
,
+ })
+
+ const handler = createRequestHandler({
+ request: new Request('http://localhost/fail'),
+ createRouter: () =>
+ createRouter({
+ routeTree: rootRoute.addChildren([failingRoute]),
+ isServer: true,
+ }),
+ })
+
+ const response = await handler(({ router, responseHeaders }) =>
+ renderRouterToString({
+ router,
+ responseHeaders,
+ children: ,
+ }),
+ )
+
+ expect(response.status).toBe(500)
+ const html = await response.text()
+ const serverDocument = new DOMParser().parseFromString(html, 'text/html')
+
+ expect(serverDocument.body.textContent).toContain(
+ 'Error UI: beforeLoad failed',
+ )
+ expect(serverDocument.head.querySelector('title')?.textContent).toBe(
+ 'Error title',
+ )
+ expect(
+ serverDocument.head.querySelector(
+ 'link[rel="stylesheet"][href="/global.css"]',
+ ),
+ ).not.toBeNull()
+})
+
describe('notFoundComponent is rendered when an error is thrown in params.parse', () => {
test('displays notFoundComponent when error is thrown in params.parse', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
@@ -423,13 +852,11 @@ describe('notFoundComponent is rendered when an error is thrown in params.parse'
render( )
- await act(() => router.latestLoadPromise)
- expect(rootLoader).toHaveBeenCalledTimes(1)
-
const linkToRottenPizza = await screen.findByRole('link', {
name: 'link to rotten pizza',
})
+ expect(rootLoader).toHaveBeenCalledTimes(1)
expect(linkToRottenPizza).toBeInTheDocument()
await act(() => fireEvent.mouseOver(linkToRottenPizza))
await act(() => fireEvent.click(linkToRottenPizza))
diff --git a/packages/react-router/tests/hydration-capped-boundary-pending.test.tsx b/packages/react-router/tests/hydration-capped-boundary-pending.test.tsx
new file mode 100644
index 0000000000..57d99c8af5
--- /dev/null
+++ b/packages/react-router/tests/hydration-capped-boundary-pending.test.tsx
@@ -0,0 +1,264 @@
+import * as React from 'react'
+import { act } from '@testing-library/react'
+import { hydrateRoot } from 'react-dom/client'
+import { renderToString } from 'react-dom/server'
+import { afterEach, describe, expect, test, vi } from 'vitest'
+import { createMemoryHistory } from '@tanstack/history'
+import { dehydrateSsrMatchId } from '../../router-core/src/ssr/ssr-match-id'
+import { hydrate } from '../src/ssr/client'
+import {
+ Outlet,
+ RouterProvider,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ notFound,
+} from '../src'
+import type { TsrSsrGlobal } from '../src/ssr/client'
+
+declare global {
+ interface Window {
+ $_TSR?: TsrSsrGlobal
+ }
+}
+
+const testCleanups: Array<() => void | Promise> = []
+
+afterEach(async () => {
+ while (testCleanups.length) {
+ await testCleanups.pop()!()
+ }
+ vi.restoreAllMocks()
+ window.$_TSR = undefined
+ document.body.innerHTML = ''
+})
+
+describe('hydrating a server-capped boundary lane', () => {
+ test('recovers a /404 payload against a missing browser URL', async () => {
+ function MissingPage() {
+ return Missing page
+ }
+
+ const makeRouteTree = () => {
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ notFoundComponent: MissingPage,
+ })
+ const notFoundRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/404',
+ component: MissingPage,
+ })
+ return rootRoute.addChildren([notFoundRoute])
+ }
+
+ const serverRouter = createRouter({
+ routeTree: makeRouteTree(),
+ history: createMemoryHistory({ initialEntries: ['/404'] }),
+ })
+ serverRouter.isServer = true
+ await serverRouter.load()
+ const serverMatches = serverRouter.stores.matches.get()
+ const serverHtml = renderToString( )
+ expect(serverHtml).toContain('Missing page')
+
+ const clientRouter = createRouter({
+ routeTree: makeRouteTree(),
+ history: createMemoryHistory({ initialEntries: ['/missing'] }),
+ })
+ window.$_TSR = {
+ router: {
+ manifest: { routes: {} },
+ dehydratedData: {},
+ matches: serverMatches.map((match) => ({
+ i: dehydrateSsrMatchId(match.id),
+ u: match.updatedAt,
+ s: match.status,
+ l: match.loaderData,
+ e: match.error,
+ ssr: match.ssr,
+ ...(match._notFound ? { g: true } : {}),
+ })),
+ },
+ h: vi.fn(),
+ e: vi.fn(),
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(clientRouter)
+
+ const container = document.createElement('div')
+ container.innerHTML = serverHtml
+ document.body.appendChild(container)
+ let root!: ReturnType
+ await act(async () => {
+ root = hydrateRoot(container, , {
+ onRecoverableError: () => {},
+ })
+ testCleanups.push(async () => {
+ await act(() => root.unmount())
+ })
+ await Promise.resolve()
+ })
+
+ expect(container).toHaveTextContent('Missing page')
+ expect(clientRouter.state.resolvedLocation?.pathname).toBe('/missing')
+ expect(clientRouter.state.matches).toHaveLength(1)
+ expect(clientRouter.state.matches[0]).toMatchObject({ _notFound: true })
+ })
+
+ test.each([
+ ['error', 'parent'],
+ ['notFound', 'parent'],
+ ['error', 'root'],
+ ['notFound', 'root'],
+ ] as const)(
+ 'keeps the server-rendered %s %s boundary visible',
+ async (outcome, boundary) => {
+ const childLoader = vi.fn(() => 'child data')
+ const boundaryCommits = vi.fn()
+
+ function BoundaryError() {
+ React.useEffect(() => {
+ boundaryCommits()
+ }, [])
+ return Boundary error
+ }
+
+ function BoundaryNotFound() {
+ React.useEffect(() => {
+ boundaryCommits()
+ }, [])
+ return Boundary not found
+ }
+
+ const makeRouteTree = () => {
+ const boundaryOptions = {
+ beforeLoad: () => {
+ throw outcome === 'notFound'
+ ? notFound()
+ : new Error('server route failure')
+ },
+ pendingComponent: () => (
+ Boundary pending
+ ),
+ errorComponent: BoundaryError,
+ notFoundComponent: BoundaryNotFound,
+ }
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ ...(boundary === 'root' ? boundaryOptions : {}),
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ component: Outlet,
+ ...(boundary === 'parent' ? boundaryOptions : {}),
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ loader: childLoader,
+ component: () => Child
,
+ })
+ return {
+ routeTree: rootRoute.addChildren([
+ parentRoute.addChildren([childRoute]),
+ ]),
+ }
+ }
+
+ const serverRouter = createRouter({
+ ...makeRouteTree(),
+ history: createMemoryHistory({
+ initialEntries: ['/parent/child'],
+ }),
+ })
+ serverRouter.isServer = true
+ await serverRouter.load()
+
+ const serverMatches = serverRouter.stores.matches.get()
+ expect(serverMatches).toHaveLength(3)
+ const serverBoundary = serverMatches[boundary === 'root' ? 0 : 1]!
+ if (outcome === 'notFound' && boundary === 'root') {
+ expect(serverBoundary).toMatchObject({
+ status: 'success',
+ _notFound: true,
+ })
+ } else {
+ expect(serverBoundary.status).toBe(outcome)
+ }
+ const serverHtml = renderToString(
+ ,
+ )
+ expect(serverHtml).toContain(
+ outcome === 'notFound' ? 'Boundary not found' : 'Boundary error',
+ )
+ expect(boundaryCommits).not.toHaveBeenCalled()
+
+ const clientRouter = createRouter({
+ ...makeRouteTree(),
+ history: createMemoryHistory({
+ initialEntries: ['/parent/child'],
+ }),
+ })
+
+ window.$_TSR = {
+ router: {
+ manifest: { routes: {} },
+ dehydratedData: {},
+ matches: serverMatches
+ .slice(0, boundary === 'root' ? 1 : 2)
+ .map((match) => ({
+ i: dehydrateSsrMatchId(match.id),
+ u: match.updatedAt,
+ s: match.status,
+ l: match.loaderData,
+ e: match.error,
+ ssr: match.ssr,
+ ...(match._notFound ? { g: true } : {}),
+ })),
+ },
+ h: vi.fn(),
+ e: vi.fn(),
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(clientRouter)
+
+ const container = document.createElement('div')
+ container.innerHTML = serverHtml
+ document.body.appendChild(container)
+ const consoleError = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {})
+ let root!: ReturnType
+ await act(async () => {
+ root = hydrateRoot(container, )
+ testCleanups.push(async () => {
+ await act(() => root.unmount())
+ })
+ await Promise.resolve()
+ })
+
+ // A shorter dehydrated lane means SPA mode only for an actual shell.
+ // Here it is shorter because the server already rendered a terminal
+ // boundary, so hydration must not replace that boundary with pending UI.
+ expect(boundaryCommits).toHaveBeenCalledTimes(1)
+ expect(container).toHaveTextContent(
+ outcome === 'notFound' ? 'Boundary not found' : 'Boundary error',
+ )
+ expect(container).not.toHaveTextContent('Boundary pending')
+ expect(consoleError.mock.calls.flat().join(' ')).not.toMatch(
+ /hydration|did not match/i,
+ )
+ expect(childLoader).not.toHaveBeenCalled()
+ },
+ )
+})
diff --git a/packages/react-router/tests/hydration-terminal-lane.test.tsx b/packages/react-router/tests/hydration-terminal-lane.test.tsx
new file mode 100644
index 0000000000..35784ea916
--- /dev/null
+++ b/packages/react-router/tests/hydration-terminal-lane.test.tsx
@@ -0,0 +1,98 @@
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, describe, expect, test, vi } from 'vitest'
+import { hydrate } from '@tanstack/router-core/ssr/client'
+import { dehydrateSsrMatchId } from '../../router-core/src/ssr/ssr-match-id'
+import {
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+import type { AnyRouteMatch } from '@tanstack/router-core'
+import type { TsrSsrGlobal } from '@tanstack/router-core/ssr/client'
+
+function bootstrap(
+ matches: Array<{
+ match: AnyRouteMatch
+ status: AnyRouteMatch['status']
+ ssr: AnyRouteMatch['ssr']
+ data?: unknown
+ error?: unknown
+ }>,
+): void {
+ window.$_TSR = {
+ router: {
+ manifest: undefined,
+ matches: matches.map(({ match, status, ssr, data, error }) => ({
+ i: dehydrateSsrMatchId(match.id),
+ l: data,
+ e: error,
+ s: status,
+ ssr,
+ u: Date.now(),
+ })),
+ },
+ h: vi.fn(),
+ e: vi.fn(),
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ } as TsrSsrGlobal
+}
+
+afterEach(() => {
+ cleanup()
+ delete window.$_TSR
+})
+
+describe('hydration terminal lane', () => {
+ test('keeps server data while loading only the missing client suffix', async () => {
+ const parentLoader = vi.fn(() => 'client-parent')
+ const childLoader = vi.fn(() => 'client-child')
+ const rootRoute = createRootRoute({ component: Outlet })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ loader: parentLoader,
+ component: () => (
+ <>
+ {parentRoute.useLoaderData()}
+
+ >
+ ),
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ ssr: false,
+ loader: childLoader,
+ component: () => {childRoute.useLoaderData()}
,
+ })
+ const router = createRouter({
+ history: createMemoryHistory({ initialEntries: ['/parent/child'] }),
+ routeTree: rootRoute.addChildren([parentRoute.addChildren([childRoute])]),
+ })
+ const matches = router.matchRoutes(router.state.location)
+ bootstrap([
+ { match: matches[0]!, status: 'success', ssr: true },
+ {
+ match: matches[1]!,
+ status: 'success',
+ ssr: true,
+ data: 'server-parent',
+ },
+ { match: matches[2]!, status: 'pending', ssr: false },
+ ])
+
+ await hydrate(router)
+ render( )
+
+ expect(await screen.findByText('server-parent')).toBeInTheDocument()
+ expect(await screen.findByText('client-child')).toBeInTheDocument()
+ expect(screen.queryByText('client-parent')).not.toBeInTheDocument()
+ expect(parentLoader).not.toHaveBeenCalled()
+ expect(childLoader).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/react-router/tests/issue-2182-root-pending.test.tsx b/packages/react-router/tests/issue-2182-root-pending.test.tsx
new file mode 100644
index 0000000000..9cf3f82cb7
--- /dev/null
+++ b/packages/react-router/tests/issue-2182-root-pending.test.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
+
+// https://github.com/TanStack/router/issues/2182
+test('root pending fallback remains visible through pendingMinMs', async () => {
+ vi.useFakeTimers()
+
+ const loaderGate = createControlledPromise()
+ const rootRoute = createRootRoute({
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => Pending
,
+ loader: () => loaderGate,
+ component: () => Loaded
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(0)
+ })
+ expect(screen.getByTestId('root-pending')).toBeInTheDocument()
+ expect(screen.queryByTestId('root-content')).not.toBeInTheDocument()
+
+ loaderGate.resolve('loaded')
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(99)
+ })
+ expect(screen.getByTestId('root-pending')).toBeInTheDocument()
+ expect(screen.queryByTestId('root-content')).not.toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ })
+ expect(screen.queryByTestId('root-pending')).not.toBeInTheDocument()
+ expect(screen.getByTestId('root-content')).toBeInTheDocument()
+})
diff --git a/packages/react-router/tests/issue-2905-root-beforeload-pending.test.tsx b/packages/react-router/tests/issue-2905-root-beforeload-pending.test.tsx
new file mode 100644
index 0000000000..2821571c42
--- /dev/null
+++ b/packages/react-router/tests/issue-2905-root-beforeload-pending.test.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
+
+// https://github.com/TanStack/router/issues/2905
+test('#2905: a root pendingComponent renders while root beforeLoad is pending', async () => {
+ vi.useFakeTimers()
+
+ const rootRoute = createRootRoute({
+ beforeLoad: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 3_000))
+ },
+ pendingComponent: () => Root pending
,
+ component: () => Root
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(0)
+ })
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(999)
+ })
+ expect(screen.queryByTestId('root-pending')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('root-content')).not.toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ })
+ expect(screen.getByTestId('root-pending')).toBeInTheDocument()
+ expect(screen.queryByTestId('root-content')).not.toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1_999)
+ })
+ expect(screen.getByTestId('root-pending')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ })
+ expect(screen.queryByTestId('root-pending')).not.toBeInTheDocument()
+ expect(screen.getByTestId('root-content')).toBeInTheDocument()
+})
diff --git a/packages/react-router/tests/issue-4467-lazy-route-pending.test.tsx b/packages/react-router/tests/issue-4467-lazy-route-pending.test.tsx
new file mode 100644
index 0000000000..ef85b2f235
--- /dev/null
+++ b/packages/react-router/tests/issue-4467-lazy-route-pending.test.tsx
@@ -0,0 +1,144 @@
+import * as React from 'react'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, onTestFinished, test, vi } from 'vitest'
+
+import {
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createLazyRoute,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(cleanup)
+
+// https://github.com/TanStack/router/issues/4467
+test('default pending component renders while lazy route options load', async () => {
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index page ,
+ })
+ const lazyPageOptions = createLazyRoute('/page')({
+ component: () => Page ,
+ })
+ const lazyOptions = createControlledPromise()
+ const loadLazyOptions = vi.fn(() => lazyOptions)
+ const pageRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ }).lazy(loadLazyOptions)
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ defaultPendingMs: 0,
+ defaultPendingMinMs: 0,
+ defaultPendingComponent: () => Loading page
,
+ })
+ let navigationPromise: Promise | undefined
+
+ onTestFinished(async () => {
+ await act(async () => {
+ if (lazyOptions.status === 'pending') {
+ lazyOptions.resolve(lazyPageOptions)
+ }
+ await navigationPromise
+ })
+ })
+
+ render( )
+
+ expect(
+ await screen.findByRole('heading', { name: 'Index page' }),
+ ).toBeInTheDocument()
+
+ act(() => {
+ navigationPromise = router.navigate({ to: '/page' })
+ })
+
+ expect(await screen.findByRole('status')).toHaveTextContent('Loading page')
+ expect(
+ screen.queryByRole('heading', { name: 'Page' }),
+ ).not.toBeInTheDocument()
+ expect(lazyOptions.status).toBe('pending')
+ expect(loadLazyOptions).toHaveBeenCalledTimes(1)
+
+ await act(async () => {
+ lazyOptions.resolve(lazyPageOptions)
+ await navigationPromise
+ })
+
+ expect(screen.getByRole('heading', { name: 'Page' })).toBeInTheDocument()
+ expect(screen.queryByRole('status')).not.toBeInTheDocument()
+ expect(loadLazyOptions).toHaveBeenCalledTimes(1)
+})
+
+test('a lazy pending component is offered while the eager loader is still pending', async () => {
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index page ,
+ })
+ const loader = createControlledPromise()
+ const lazyPageOptions = createLazyRoute('/page')({
+ pendingComponent: () => Loading lazy page
,
+ component: () => Page ,
+ })
+ const lazyOptions = createControlledPromise()
+ const pageRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ loader: () => loader,
+ }).lazy(() => lazyOptions)
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ defaultPendingMs: 0,
+ defaultPendingMinMs: 0,
+ defaultPendingComponent: () => Loading default
,
+ })
+ let navigation: Promise | undefined
+
+ onTestFinished(async () => {
+ await act(async () => {
+ lazyOptions.resolve(lazyPageOptions)
+ loader.resolve()
+ await navigation
+ })
+ })
+
+ render( )
+ expect(
+ await screen.findByRole('heading', { name: 'Index page' }),
+ ).toBeInTheDocument()
+
+ act(() => {
+ navigation = router.navigate({ to: '/page' })
+ })
+ expect(await screen.findByRole('status')).toHaveTextContent('Loading default')
+
+ await act(async () => {
+ lazyOptions.resolve(lazyPageOptions)
+ })
+
+ expect(await screen.findByRole('status')).toHaveTextContent(
+ 'Loading lazy page',
+ )
+ expect(
+ screen.queryByRole('heading', { name: 'Page' }),
+ ).not.toBeInTheDocument()
+
+ await act(async () => {
+ loader.resolve()
+ await navigation
+ })
+
+ expect(screen.getByRole('heading', { name: 'Page' })).toBeInTheDocument()
+})
diff --git a/packages/react-router/tests/issue-4476-react-query-cancellation.test.tsx b/packages/react-router/tests/issue-4476-react-query-cancellation.test.tsx
new file mode 100644
index 0000000000..039608ffb2
--- /dev/null
+++ b/packages/react-router/tests/issue-4476-react-query-cancellation.test.tsx
@@ -0,0 +1,129 @@
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
+import {
+ QueryClient,
+ QueryClientProvider,
+ useQuery,
+} from '@tanstack/react-query'
+import { afterEach, expect, onTestFinished, test, vi } from 'vitest'
+import {
+ Link,
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(cleanup)
+
+// https://github.com/TanStack/router/issues/4476
+test('#4476: pending navigation keeps the query observer mounted and its fetchQuery signal alive', async () => {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, staleTime: 0 } },
+ })
+ const queryGate = createControlledPromise()
+ const queryKey = ['issue-4476'] as const
+ const routeError = vi.fn()
+ const pageTwoBeforeLoad = vi.fn()
+ const pendingComponentRendered = vi.fn()
+ let querySignal: AbortSignal | undefined
+
+ const rootRoute = createRootRoute({
+ component: () => (
+ <>
+
+ Page two
+
+
+ >
+ ),
+ })
+ function PageOneComponent() {
+ const query = useQuery({
+ queryKey,
+ queryFn: () => Promise.resolve(3),
+ })
+ return Page one: {query.data}
+ }
+ const pageOneRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: PageOneComponent,
+ })
+ const pageTwoRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page-two',
+ beforeLoad: async ({ preload }) => {
+ pageTwoBeforeLoad(preload)
+ if (preload) {
+ return
+ }
+ const data = await queryClient.fetchQuery({
+ queryKey,
+ queryFn: ({ signal }) => {
+ querySignal = signal
+ return queryGate
+ },
+ })
+ return { data }
+ },
+ errorComponent: ({ error }) => {
+ routeError(error)
+ return {error.name}
+ },
+ component: () => {
+ const { data } = pageTwoRoute.useRouteContext()
+ return Page two: {data}
+ },
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([pageOneRoute, pageTwoRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ defaultPreload: 'intent',
+ defaultPreloadDelay: 0,
+ defaultPreloadStaleTime: 0,
+ defaultPendingMs: 0,
+ defaultPendingComponent: () => {
+ pendingComponentRendered()
+ return Loading page two
+ },
+ })
+
+ onTestFinished(() => {
+ queryGate.resolve(10)
+ queryClient.clear()
+ })
+
+ render(
+
+
+ ,
+ )
+ expect(await screen.findByText('Page one: 3')).toBeInTheDocument()
+
+ const link = screen.getByRole('link', { name: 'Page two' })
+ fireEvent.mouseOver(link)
+ await waitFor(() => expect(pageTwoBeforeLoad).toHaveBeenCalledWith(true))
+ fireEvent.click(link)
+
+ await waitFor(() => expect(querySignal).toBeDefined())
+ expect(screen.getByTestId('page-one')).toBeInTheDocument()
+ expect(screen.getByTestId('page-two-pending')).toBeInTheDocument()
+ expect(querySignal?.aborted).toBe(false)
+ queryGate.resolve(10)
+
+ expect(await screen.findByText('Page two: 10')).toBeInTheDocument()
+ expect(routeError).not.toHaveBeenCalled()
+ expect(screen.queryByTestId('page-one')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('page-two-pending')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('page-two-error')).not.toBeInTheDocument()
+ expect(router.state.location.pathname).toBe('/page-two')
+})
diff --git a/packages/react-router/tests/issue-4759-pending-frame.test.tsx b/packages/react-router/tests/issue-4759-pending-frame.test.tsx
new file mode 100644
index 0000000000..7b8b74161c
--- /dev/null
+++ b/packages/react-router/tests/issue-4759-pending-frame.test.tsx
@@ -0,0 +1,100 @@
+import * as React from 'react'
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ onTestFinished,
+ test,
+ vi,
+} from 'vitest'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import {
+ RouterProvider,
+ createBrowserHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+import type { RouterHistory } from '../src'
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+ expect(window.location.pathname).toBe('/')
+})
+
+afterEach(() => {
+ history.destroy()
+ window.history.replaceState(null, 'root', '/')
+ vi.resetAllMocks()
+ cleanup()
+})
+
+// Repro for https://github.com/TanStack/router/issues/4759
+//
+// JSDOM cannot observe browser paints. This unit reduction verifies the event
+// ordering behind the issue: pending DOM must be published before the first
+// macrotask when pendingMs is 0.
+describe('issue #4759: pendingMs 0 publishes pending DOM before a macrotask', () => {
+ test('pending fallback is committed on mount without waiting for a macrotask', async () => {
+ vi.useFakeTimers()
+ let resolveLoader!: (value: string) => void
+ const loaderPromise = new Promise((resolve) => {
+ resolveLoader = resolve
+ })
+
+ const rootRoute = createRootRoute()
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: () => loaderPromise,
+ component: () => loaded
,
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history,
+ })
+ let resolveRendered!: () => void
+ const rendered = new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ const unsubscribe = router.subscribe('onRendered', (event) => {
+ if (event.toLocation.pathname === '/') {
+ resolveRendered()
+ }
+ })
+
+ onTestFinished(() => {
+ unsubscribe()
+ resolveLoader('done')
+ vi.useRealTimers()
+ })
+
+ render(
+
+ (
+ pending...
+ )}
+ />
+ ,
+ )
+
+ // Fake timers keep the first macrotask frozen. An implementation that
+ // publishes pending state with setTimeout cannot satisfy this assertion.
+ await act(async () => {})
+ expect(screen.getByTestId('pending')).toBeInTheDocument()
+
+ // Sanity: the load still completes normally afterwards.
+ resolveLoader('done')
+ await act(() => rendered)
+ expect(screen.getByTestId('loaded')).toBeInTheDocument()
+ expect(screen.queryByTestId('pending')).not.toBeInTheDocument()
+ })
+})
diff --git a/packages/react-router/tests/issue-5778-router-provider-context-preload.test.tsx b/packages/react-router/tests/issue-5778-router-provider-context-preload.test.tsx
new file mode 100644
index 0000000000..e179a1502e
--- /dev/null
+++ b/packages/react-router/tests/issue-5778-router-provider-context-preload.test.tsx
@@ -0,0 +1,113 @@
+import * as React from 'react'
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
+import { afterEach, expect, test } from 'vitest'
+import {
+ Link,
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRouteWithContext,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(cleanup)
+
+// https://github.com/TanStack/router/issues/5778
+test('#5778: intent preload sees a RouterProvider context update before the first navigation', async () => {
+ const seen: Array<{
+ route: 'auth' | 'foo'
+ foo: string
+ cause: string
+ preload: boolean
+ }> = []
+ const rootRoute = createRootRouteWithContext<{ foo: string }>()({
+ component: () => (
+ <>
+
+ Foo
+
+
+ >
+ ),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const authRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: '_authenticated',
+ beforeLoad: ({ context, cause, preload }) => {
+ seen.push({ route: 'auth', foo: context.foo, cause, preload })
+ },
+ component: () => ,
+ })
+ const fooRoute = createRoute({
+ getParentRoute: () => authRoute,
+ path: '/foo',
+ beforeLoad: ({ context, cause, preload }) => {
+ seen.push({ route: 'foo', foo: context.foo, cause, preload })
+ },
+ component: () => Foo page
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ authRoute.addChildren([fooRoute]),
+ ]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ context: { foo: null! },
+ defaultPreload: 'intent',
+ defaultPreloadDelay: 0,
+ })
+
+ function App() {
+ const [foo, setFoo] = React.useState('foo')
+ return (
+ <>
+ {foo}
+ {
+ setFoo('baz')
+ }}
+ >
+ Update context
+
+
+ >
+ )
+ }
+
+ render( )
+ expect(await screen.findByText('Home')).toBeInTheDocument()
+ expect(screen.getByLabelText('Current context')).toHaveTextContent('foo')
+
+ fireEvent.click(screen.getByRole('button', { name: 'Update context' }))
+ await waitFor(() =>
+ expect(screen.getByLabelText('Current context')).toHaveTextContent('baz'),
+ )
+
+ expect(seen).toEqual([])
+ const link = screen.getByRole('link', { name: 'Foo' })
+ fireEvent.mouseEnter(link)
+ await waitFor(() => expect(seen).toHaveLength(2))
+ expect(seen).toEqual([
+ { route: 'auth', foo: 'baz', cause: 'preload', preload: true },
+ { route: 'foo', foo: 'baz', cause: 'preload', preload: true },
+ ])
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.queryByText('Foo page')).not.toBeInTheDocument()
+ expect(router.state.location.pathname).toBe('/')
+
+ fireEvent.click(link)
+ expect(await screen.findByText('Foo page')).toBeInTheDocument()
+ expect(router.state.location.pathname).toBe('/foo')
+})
diff --git a/packages/react-router/tests/issue-6107-lazy-chunk-error-component.test.tsx b/packages/react-router/tests/issue-6107-lazy-chunk-error-component.test.tsx
new file mode 100644
index 0000000000..22440fe119
--- /dev/null
+++ b/packages/react-router/tests/issue-6107-lazy-chunk-error-component.test.tsx
@@ -0,0 +1,89 @@
+import * as React from 'react'
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ Link,
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ vi.restoreAllMocks()
+ cleanup()
+})
+
+// https://github.com/TanStack/router/issues/6107
+test('#6107: lazy chunk hover failure is non-fatal and navigation renders defaultErrorComponent', async () => {
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+ const chunkError = new TypeError(
+ 'Failed to fetch dynamically imported module: /assets/posts.lazy.js',
+ )
+ const defaultErrorRendered = vi.fn()
+ let lazyCalls = 0
+
+ const rootRoute = createRootRoute({
+ component: () => (
+ <>
+
+ Posts
+
+
+ >
+ ),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/posts',
+ }).lazy(() => {
+ lazyCalls++
+ return Promise.reject(chunkError)
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, postsRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ defaultPreloadDelay: 0,
+ defaultErrorComponent: ({ error }) => {
+ defaultErrorRendered(error)
+ return {error.message}
+ },
+ })
+ const preloadRoute = vi.spyOn(router, 'preloadRoute')
+
+ render( )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+
+ const link = screen.getByRole('link', { name: 'Posts' })
+ fireEvent.mouseEnter(link)
+ await waitFor(() => expect(preloadRoute).toHaveBeenCalledTimes(1))
+ await preloadRoute.mock.results[0]!.value
+ expect(lazyCalls).toBeGreaterThanOrEqual(1)
+ expect(screen.getByText('Index')).toBeInTheDocument()
+ expect(screen.queryByTestId('default-error')).not.toBeInTheDocument()
+ expect(defaultErrorRendered).not.toHaveBeenCalled()
+
+ const callsAfterPreload = lazyCalls
+ fireEvent.click(link)
+ expect(await screen.findByTestId('default-error')).toHaveTextContent(
+ chunkError.message,
+ )
+ expect(lazyCalls).toBeGreaterThan(callsAfterPreload)
+ expect(defaultErrorRendered).toHaveBeenCalledWith(chunkError)
+ expect(screen.queryByText('Index')).not.toBeInTheDocument()
+ expect(router.state.location.pathname).toBe('/posts')
+ expect(router.state.status).toBe('idle')
+})
diff --git a/packages/react-router/tests/issue-6371-search-default-normalization-abort.test.tsx b/packages/react-router/tests/issue-6371-search-default-normalization-abort.test.tsx
new file mode 100644
index 0000000000..7698d117a6
--- /dev/null
+++ b/packages/react-router/tests/issue-6371-search-default-normalization-abort.test.tsx
@@ -0,0 +1,169 @@
+import { afterEach, expect, onTestFinished, test, vi } from 'vitest'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import {
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ useLocation,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+})
+
+test('#6371: initial search defaults produce one live canonical loader', async () => {
+ const loaderGate = createControlledPromise()
+ const canonicalLocation = createControlledPromise()
+ const abortedLoaderData = 'discarded aborted loader'
+ const loaderSignals: Array = []
+ const loaderLocations: Array = []
+ const errorComponentRendered = vi.fn()
+ const loader = vi.fn(
+ ({
+ abortController,
+ location,
+ }: {
+ abortController: AbortController
+ location: { href: string }
+ }) => {
+ const signal = abortController.signal
+ loaderSignals.push(signal)
+ loaderLocations.push(location.href)
+
+ return new Promise((resolve, reject) => {
+ const onAbort = () => {
+ resolve(abortedLoaderData)
+ }
+
+ if (signal.aborted) {
+ onAbort()
+ return
+ }
+
+ signal.addEventListener('abort', onAbort, { once: true })
+ loaderGate.then((data) => {
+ signal.removeEventListener('abort', onAbort)
+ resolve(data)
+ }, reject)
+ })
+ },
+ )
+
+ const PendingLocation = () => {
+ const href = useLocation({ select: (location) => location.href })
+ return {href}
+ }
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+ const aboutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ validateSearch: (search: Record) => ({
+ page: typeof search.page === 'number' ? search.page : 1,
+ }),
+ loader,
+ component: () => (
+ {aboutRoute.useLoaderData()}
+ ),
+ errorComponent: ({ error }) => {
+ errorComponentRendered(error)
+ return {error.message}
+ },
+ })
+ const history = createMemoryHistory({ initialEntries: ['/about'] })
+ const unsubscribeHistory = history.subscribe(() => {
+ if (history.location.href === '/about?page=1') {
+ canonicalLocation.resolve()
+ }
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([aboutRoute]),
+ history,
+ defaultPendingMs: 0,
+ defaultPendingMinMs: 0,
+ defaultPendingComponent: PendingLocation,
+ })
+
+ onTestFinished(async () => {
+ unsubscribeHistory()
+ await act(() => {
+ canonicalLocation.resolve()
+ loaderGate.resolve('about data')
+ })
+ })
+
+ render( )
+
+ await act(async () => {
+ await canonicalLocation
+ })
+
+ expect(loader).toHaveBeenCalledTimes(1)
+ expect(loaderLocations).toEqual(['/about?page=1'])
+ expect(loaderSignals).toHaveLength(1)
+ expect(loaderSignals[0]?.aborted).toBe(false)
+
+ expect(
+ await screen.findByText('/about?page=1', {
+ selector: '[data-testid="pending-location"]',
+ }),
+ ).toBeInTheDocument()
+
+ await act(() => {
+ loaderGate.resolve('about data')
+ })
+
+ expect(await screen.findByTestId('about-data')).toHaveTextContent(
+ 'about data',
+ )
+ expect(loader).toHaveBeenCalledTimes(1)
+ expect(loaderSignals[0]?.aborted).toBe(false)
+ expect(errorComponentRendered).not.toHaveBeenCalled()
+ expect(screen.queryByTestId('about-error')).not.toBeInTheDocument()
+})
+
+test('initial canonicalization bypasses existing navigation blockers', async () => {
+ const loader = vi.fn(
+ ({ location }: { location: { href: string } }) => location.href,
+ )
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+ const aboutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ validateSearch: (search: Record) => ({
+ page: typeof search.page === 'number' ? search.page : 1,
+ }),
+ loader,
+ component: () => {aboutRoute.useLoaderData()}
,
+ })
+ const history = createMemoryHistory({ initialEntries: ['/about'] })
+ const blockerFn = vi.fn(() => true)
+ const unblock = history.block({
+ blockerFn,
+ enableBeforeUnload: false,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([aboutRoute]),
+ history,
+ })
+
+ onTestFinished(unblock)
+
+ render( )
+
+ expect(await screen.findByText('/about?page=1')).toBeInTheDocument()
+ expect(loader).toHaveBeenCalledTimes(1)
+ expect(history.location.href).toBe('/about?page=1')
+ expect(router.latestLocation.href).toBe('/about?page=1')
+ expect(router.state.location.href).toBe('/about?page=1')
+ expect(router.state.resolvedLocation?.href).toBe('/about?page=1')
+ expect(blockerFn).not.toHaveBeenCalled()
+})
diff --git a/packages/react-router/tests/issue-7051-force-pending-suspense.test.tsx b/packages/react-router/tests/issue-7051-force-pending-suspense.test.tsx
new file mode 100644
index 0000000000..5640cb7477
--- /dev/null
+++ b/packages/react-router/tests/issue-7051-force-pending-suspense.test.tsx
@@ -0,0 +1,195 @@
+import { act } from 'react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { cleanup, render, screen } from '@testing-library/react'
+import {
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ vi.clearAllMocks()
+ cleanup()
+})
+
+// Ported from PR #7051. A forced-pending reload must keep showing its pending
+// fallback until fresh content commits instead of exposing the error boundary.
+test('invalidate({ forcePending: true }) keeps rendering the pending fallback instead of the error boundary', async () => {
+ const history = createMemoryHistory({
+ initialEntries: ['/force-pending'],
+ })
+ const errorComponentRendered = vi.fn()
+ let shouldSuspendReload = false
+ const reloadGate = createControlledPromise()
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+
+ const forcePendingRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/force-pending',
+ pendingMs: 0,
+ pendingMinMs: 10,
+ loader: async () => {
+ if (shouldSuspendReload) {
+ await reloadGate
+ }
+
+ return 'done'
+ },
+ component: () => (
+
+ {forcePendingRoute.useLoaderData()}
+
+ ),
+ pendingComponent: () => (
+ Pending...
+ ),
+ errorComponent: ({ error }) => {
+ errorComponentRendered(error)
+ return {String(error)}
+ },
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([forcePendingRoute]),
+ history,
+ })
+
+ render( )
+
+ await act(() => router.load())
+ expect(await screen.findByTestId('force-pending-route')).toHaveTextContent(
+ 'done',
+ )
+
+ shouldSuspendReload = true
+ let invalidation!: Promise
+ act(() => {
+ invalidation = router.invalidate({ forcePending: true })
+ })
+
+ expect(
+ await screen.findByTestId('force-pending-fallback'),
+ ).toBeInTheDocument()
+ expect(errorComponentRendered).not.toHaveBeenCalled()
+ expect(screen.queryByTestId('force-pending-error')).not.toBeInTheDocument()
+
+ act(() => {
+ reloadGate.resolve()
+ })
+
+ await act(() => invalidation)
+ expect(await screen.findByTestId('force-pending-route')).toHaveTextContent(
+ 'done',
+ )
+ expect(screen.queryByTestId('force-pending-fallback')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('force-pending-error')).not.toBeInTheDocument()
+ expect(errorComponentRendered).not.toHaveBeenCalled()
+ expect(router.state.location.pathname).toBe('/force-pending')
+ expect(router.state.status).toBe('idle')
+})
+
+test('regular navigation keeps the current pending fallback while its loader is aborted', async () => {
+ const firstLoaderAborted = createControlledPromise()
+ const secondLoaderStarted = createControlledPromise()
+ const secondLoaderGate = createControlledPromise()
+ const firstErrorComponentRendered = vi.fn()
+ let firstSignal: AbortSignal | undefined
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home page
,
+ })
+ const firstRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/first',
+ pendingMs: 0,
+ loader: async ({ abortController }) => {
+ firstSignal = abortController.signal
+ await new Promise((_resolve, reject) => {
+ abortController.signal.addEventListener(
+ 'abort',
+ () => {
+ firstLoaderAborted.resolve()
+ reject(new DOMException('Aborted', 'AbortError'))
+ },
+ { once: true },
+ )
+ })
+ return 'first'
+ },
+ component: () => (
+ {firstRoute.useLoaderData()}
+ ),
+ pendingComponent: () => (
+ Pending first route
+ ),
+ errorComponent: ({ error }) => {
+ firstErrorComponentRendered(error)
+ return {String(error)}
+ },
+ })
+ const secondRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/second',
+ loader: async () => {
+ secondLoaderStarted.resolve()
+ await secondLoaderGate
+ return 'second'
+ },
+ component: () => (
+ {secondRoute.useLoaderData()}
+ ),
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, firstRoute, secondRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ defaultPreload: false,
+ })
+
+ render( )
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
+
+ act(() => {
+ void router.navigate({ to: '/first' })
+ })
+ expect(await screen.findByTestId('first-pending')).toBeInTheDocument()
+ expect(firstSignal?.aborted).toBe(false)
+
+ let secondNavigation!: Promise
+ act(() => {
+ secondNavigation = router.navigate({ to: '/second' })
+ })
+ await act(async () => {
+ await Promise.all([firstLoaderAborted, secondLoaderStarted])
+ })
+
+ expect(firstSignal?.aborted).toBe(true)
+ expect(screen.getByTestId('first-pending')).toBeInTheDocument()
+ expect(screen.queryByTestId('first-error')).not.toBeInTheDocument()
+ expect(firstErrorComponentRendered).not.toHaveBeenCalled()
+ expect(router.state.location.pathname).toBe('/second')
+ expect(router.state.status).toBe('pending')
+
+ act(() => {
+ secondLoaderGate.resolve()
+ })
+ await act(() => secondNavigation)
+
+ expect(await screen.findByTestId('second-page')).toHaveTextContent('second')
+ expect(screen.queryByTestId('first-pending')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('first-error')).not.toBeInTheDocument()
+ expect(firstErrorComponentRendered).not.toHaveBeenCalled()
+ expect(router.state.location.pathname).toBe('/second')
+ expect(router.state.status).toBe('idle')
+})
diff --git a/packages/react-router/tests/issue-7367-pending-min-redirect.test.tsx b/packages/react-router/tests/issue-7367-pending-min-redirect.test.tsx
new file mode 100644
index 0000000000..666f1d3e4b
--- /dev/null
+++ b/packages/react-router/tests/issue-7367-pending-min-redirect.test.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react'
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+
+import {
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ redirect,
+} from '../src'
+import { sleep } from './utils'
+
+afterEach(() => {
+ vi.restoreAllMocks()
+ cleanup()
+})
+
+// https://github.com/TanStack/router/issues/7367
+// Root route shows a spinner immediately (pendingMs: 0) while beforeLoad
+// decides where to send the user, keeps it up for pendingMinMs, and then
+// redirects. This used to crash in MatchInnerImpl (white screen) because the
+// redirected match was rendered/thrown after its loadPromise was cleared.
+test('immediate pending spinner (pendingMs: 0 + pendingMinMs) with root beforeLoad redirect renders the target without render errors', async () => {
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
+ let hasRedirected = false
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => loading
,
+ errorComponent: ({ error }) => (
+ {String(error)}
+ ),
+ beforeLoad: async () => {
+ await sleep(50)
+ if (!hasRedirected) {
+ hasRedirected = true
+ throw redirect({ to: '/welcome', replace: true })
+ }
+ },
+ })
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+
+ const welcomeRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/welcome',
+ component: () => Welcome
,
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, welcomeRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+
+ // pendingMs: 0 — the spinner must show right away.
+ expect(await screen.findByTestId('pending')).toBeInTheDocument()
+
+ // The redirect must complete: the target renders, no error boundary output
+ // and no render crash.
+ expect(
+ await screen.findByTestId('welcome-page', undefined, { timeout: 5_000 }),
+ ).toBeInTheDocument()
+ expect(screen.queryByTestId('pending')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('root-error')).not.toBeInTheDocument()
+ expect(router.state.location.pathname).toBe('/welcome')
+ expect(consoleError).not.toHaveBeenCalled()
+})
diff --git a/packages/react-router/tests/issue-7635-error-head-after-navigation.test.tsx b/packages/react-router/tests/issue-7635-error-head-after-navigation.test.tsx
new file mode 100644
index 0000000000..56aed7b375
--- /dev/null
+++ b/packages/react-router/tests/issue-7635-error-head-after-navigation.test.tsx
@@ -0,0 +1,100 @@
+import { createPortal } from 'react-dom'
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+ document.head.innerHTML = ''
+})
+
+// https://github.com/TanStack/router/issues/7635
+test('#7635: a parent beforeLoad error replaces the previous child title', async () => {
+ const appError = new Error('App beforeLoad failed')
+ const appErrorRendered = vi.fn()
+ const childHead = vi.fn(() => ({
+ meta: [{ title: 'Child success title' }],
+ }))
+
+ const rootRoute = createRootRoute({
+ component: () => (
+ <>
+ {createPortal( , document.head)}
+
+ Fail app load
+
+
+ >
+ ),
+ })
+ const appRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ id: '_app',
+ validateSearch: (search: Record) => ({
+ fail: search.fail === true || search.fail === 'true',
+ }),
+ beforeLoad: ({ search }) => {
+ if (search.fail) {
+ throw appError
+ }
+ },
+ head: ({ match }) => ({
+ meta: [
+ {
+ title: match.error ? 'App error title' : 'App success title',
+ },
+ ],
+ }),
+ component: Outlet,
+ errorComponent: ({ error }) => {
+ appErrorRendered(error)
+ return {error.message}
+ },
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => appRoute,
+ path: '/child',
+ head: childHead,
+ component: () => Child content
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([appRoute.addChildren([childRoute])]),
+ history: createMemoryHistory({
+ initialEntries: ['/child?fail=false'],
+ }),
+ })
+
+ render( )
+
+ expect(await screen.findByTestId('child-content')).toBeInTheDocument()
+ await waitFor(() => expect(document.title).toBe('Child success title'))
+ expect(childHead).toHaveBeenCalled()
+ childHead.mockClear()
+
+ fireEvent.click(screen.getByRole('link', { name: 'Fail app load' }))
+
+ expect(await screen.findByTestId('app-error')).toHaveTextContent(
+ appError.message,
+ )
+ expect(appErrorRendered).toHaveBeenCalledWith(appError)
+ expect(screen.queryByTestId('child-content')).not.toBeInTheDocument()
+ await waitFor(() => expect(document.title).toBe('App error title'))
+ expect(childHead).not.toHaveBeenCalled()
+ expect(router.state.location.href).toBe('/child?fail=true')
+ expect(router.state.status).toBe('idle')
+})
diff --git a/packages/react-router/tests/issue-7638-invalidate-transition-error.test.tsx b/packages/react-router/tests/issue-7638-invalidate-transition-error.test.tsx
new file mode 100644
index 0000000000..9f1c0eec3f
--- /dev/null
+++ b/packages/react-router/tests/issue-7638-invalidate-transition-error.test.tsx
@@ -0,0 +1,185 @@
+import * as React from 'react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ act,
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
+import {
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ useRouter,
+} from '../src'
+import type { ErrorComponentProps } from '../src'
+
+afterEach(() => {
+ cleanup()
+})
+
+// https://github.com/TanStack/router/issues/7638
+// router.invalidate() called inside React.startTransition while a nested
+// route is showing its errorComponent must complete the reload and land back
+// on the error UI without crashing React with
+// "Rendered more hooks than during the previous render."
+function setup({ failVia }: { failVia: 'render' | 'loader' }) {
+ const rootRoute = createRootRoute({ component: () => })
+ const parentAction = vi.fn()
+ const secondChildLoad = createControlledPromise()
+ let invalidation: Promise | undefined
+
+ const testRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/test',
+ component: function TestComponent() {
+ testRoute.useLoaderData()
+ const router = useRouter()
+ const [isPending, startTransition] = React.useTransition()
+ return (
+
+
+ startTransition(() => {
+ invalidation = router.invalidate()
+ return invalidation
+ })
+ }
+ >
+ {isPending ? 'pending' : 'invalidate'}
+
+
+ parent action
+
+
+
+ )
+ },
+ })
+
+ let childLoaderCalls = 0
+ const childLoader = vi.fn(async () => {
+ childLoaderCalls++
+ if (childLoaderCalls === 2) {
+ await secondChildLoad
+ }
+ if (failVia === 'loader') {
+ throw new Error('loader error')
+ }
+ return 'data'
+ })
+
+ const testIndexRoute = createRoute({
+ getParentRoute: () => testRoute,
+ path: '/',
+ loader: childLoader,
+ component: function ChildComponent() {
+ if (failVia === 'render') {
+ throw new Error('render error')
+ }
+ return child content
+ },
+ })
+
+ let errorRenders = 0
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([testRoute.addChildren([testIndexRoute])]),
+ history: createMemoryHistory({ initialEntries: ['/test'] }),
+ defaultErrorComponent: (props: ErrorComponentProps) => {
+ errorRenders++
+ return error: {props.error.message}
+ },
+ })
+
+ return {
+ router,
+ childLoader,
+ parentAction,
+ secondChildLoad,
+ getErrorRenders: () => errorRenders,
+ getInvalidation: () => invalidation,
+ }
+}
+
+test.each(['render', 'loader'] as const)(
+ 'invalidate() inside startTransition through a nested %s-error route does not crash',
+ async (failVia) => {
+ // Error boundaries log caught errors through console.error, and so does a
+ // hooks-order crash. Capture instead of polluting the test output, then
+ // inspect the captured calls for the crash signature.
+ const {
+ router,
+ childLoader,
+ parentAction,
+ secondChildLoad,
+ getErrorRenders,
+ getInvalidation,
+ } = setup({ failVia })
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ try {
+ render( )
+
+ expect(await screen.findByTestId('error-ui')).toHaveTextContent(
+ `error: ${failVia} error`,
+ )
+ const initialErrorRenders = getErrorRenders()
+ expect(childLoader).toHaveBeenCalledTimes(1)
+ consoleError.mockClear()
+
+ fireEvent.click(screen.getByTestId('invalidate'))
+
+ await waitFor(() => {
+ expect(childLoader).toHaveBeenCalledTimes(2)
+ expect(screen.getByTestId('invalidate')).toHaveTextContent('pending')
+ expect(screen.getByTestId('invalidate')).toBeDisabled()
+ })
+ expect(secondChildLoad.status).toBe('pending')
+
+ const invalidation = getInvalidation()
+ if (!invalidation) {
+ throw new Error('invalidate action did not return its promise')
+ }
+
+ await act(async () => {
+ secondChildLoad.resolve()
+ await invalidation
+ })
+
+ await waitFor(() => {
+ expect(screen.getByTestId('error-ui')).toHaveTextContent(
+ `error: ${failVia} error`,
+ )
+ expect(getErrorRenders()).toBeGreaterThan(initialErrorRenders)
+ expect(screen.getByTestId('invalidate')).toHaveTextContent('invalidate')
+ expect(screen.getByTestId('invalidate')).toBeEnabled()
+ })
+
+ fireEvent.click(screen.getByTestId('parent-action'))
+ expect(parentAction).toHaveBeenCalledTimes(1)
+
+ const hooksCrash = consoleError.mock.calls.find((call) =>
+ call.some((arg) =>
+ String(arg?.message ?? arg).includes('Rendered more hooks'),
+ ),
+ )
+ expect(hooksCrash).toBeUndefined()
+ } finally {
+ if (secondChildLoad.status === 'pending') {
+ await act(async () => {
+ secondChildLoad.resolve()
+ await getInvalidation()?.catch(() => undefined)
+ })
+ }
+ consoleError.mockRestore()
+ }
+ },
+)
diff --git a/packages/react-router/tests/loaders.test.tsx b/packages/react-router/tests/loaders.test.tsx
index d9b5968d72..6859b963e2 100644
--- a/packages/react-router/tests/loaders.test.tsx
+++ b/packages/react-router/tests/loaders.test.tsx
@@ -647,7 +647,7 @@ test('reproducer #4546', async () => {
}
})
-test('clears pendingTimeout when match resolves', async () => {
+test('does not show pending UI when loaders finish before their pending delays', async () => {
const defaultPendingComponentOnMountMock = vi.fn()
const nestedPendingComponentOnMountMock = vi.fn()
const fooPendingComponentOnMountMock = vi.fn()
@@ -716,7 +716,6 @@ test('clears pendingTimeout when match resolves', async () => {
})
render( )
- await act(() => router.latestLoadPromise)
const linkToFoo = await screen.findByTestId('link-to-foo')
fireEvent.click(linkToFoo)
const fooElement = await screen.findByText('Nested Foo page')
@@ -730,20 +729,32 @@ test('clears pendingTimeout when match resolves', async () => {
expect(fooPendingComponentOnMountMock).not.toHaveBeenCalled()
})
-test('throw abortError from loader upon initial load with basepath', async () => {
- window.history.replaceState(null, 'root', '/app')
+// https://github.com/TanStack/router/pull/7673
+test('#7673: a spontaneous loader AbortError renders the boundary without executing the route component', async () => {
+ history.replace('/app')
+ history.flush()
const rootRoute = createRootRoute({})
+ const abortError = new DOMException('Aborted', 'AbortError')
+ const routeComponentRendered = vi.fn()
+ const renderedError = vi.fn()
+ let routeSignal: AbortSignal | undefined
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
- loader: async () => {
- return Promise.reject(new DOMException('Aborted', 'AbortError'))
+ loader: async ({ abortController }): Promise<{ value: string }> => {
+ routeSignal = abortController.signal
+ return Promise.reject(abortError)
+ },
+ component: () => {
+ routeComponentRendered()
+ const data = indexRoute.useLoaderData()
+ return {data.value}
+ },
+ errorComponent: ({ error }) => {
+ renderedError(error)
+ return indexErrorComponent
},
- component: () => Index route content
,
- errorComponent: () => (
- indexErrorComponent
- ),
})
const routeTree = rootRoute.addChildren([indexRoute])
@@ -751,13 +762,21 @@ test('throw abortError from loader upon initial load with basepath', async () =>
render( )
- const indexElement = await screen.findByText('Index route content')
- expect(indexElement).toBeInTheDocument()
- expect(screen.queryByTestId('index-error')).not.toBeInTheDocument()
- expect(window.location.pathname.startsWith('/app')).toBe(true)
+ expect(await screen.findByTestId('index-error')).toBeInTheDocument()
+ expect(screen.queryByTestId('index-content')).not.toBeInTheDocument()
+ expect(routeComponentRendered).not.toHaveBeenCalled()
+ expect(renderedError).toHaveBeenCalledWith(abortError)
+ expect(routeSignal?.aborted).toBe(false)
+ expect(
+ router.state.matches.find((match) => match.routeId === indexRoute.id),
+ ).toMatchObject({
+ status: 'error',
+ error: abortError,
+ })
+ expect(window.location.pathname).toBe('/app')
})
-test('cancelMatches after pending timeout', async () => {
+test('navigating away from a pending route aborts its loader', async () => {
function getPendingComponent(onMount: () => void) {
const PendingComponent = () => {
useEffect(() => {
@@ -770,6 +789,7 @@ test('cancelMatches after pending timeout', async () => {
}
const onAbortMock = vi.fn()
const fooPendingComponentOnMountMock = vi.fn()
+ let fooSignal: AbortSignal | undefined
const rootRoute = createRootRoute({
component: () => (
@@ -787,17 +807,18 @@ test('cancelMatches after pending timeout', async () => {
const fooRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/foo',
- pendingMs: WAIT_TIME * 20,
+ pendingMs: 0,
loader: async ({ abortController }) => {
+ fooSignal = abortController.signal
await new Promise
((resolve) => {
- const timer = setTimeout(() => {
- resolve()
- }, WAIT_TIME * 40)
- abortController.signal.addEventListener('abort', () => {
- onAbortMock()
- clearTimeout(timer)
- resolve()
- })
+ abortController.signal.addEventListener(
+ 'abort',
+ () => {
+ onAbortMock()
+ resolve()
+ },
+ { once: true },
+ )
})
},
pendingComponent: getPendingComponent(fooPendingComponentOnMountMock),
@@ -811,18 +832,22 @@ test('cancelMatches after pending timeout', async () => {
const routeTree = rootRoute.addChildren([fooRoute, barRoute])
const router = createRouter({ routeTree, history })
render( )
- await act(() => router.latestLoadPromise)
const fooLink = await screen.findByTestId('link-to-foo')
fireEvent.click(fooLink)
- await sleep(WAIT_TIME * 30)
const pendingElement = await screen.findByText('Pending...')
expect(pendingElement).toBeInTheDocument()
- const barLink = await screen.findByTestId('link-to-bar')
- fireEvent.click(barLink)
- const barElement = await screen.findByText('Bar page')
+ expect(fooSignal?.aborted).toBe(false)
+ await act(() => router.navigate({ to: '/bar' }))
+ const barElement = screen.getByText('Bar page')
expect(barElement).toBeInTheDocument()
+
expect(fooPendingComponentOnMountMock).toHaveBeenCalled()
- expect(onAbortMock).toHaveBeenCalled()
+ expect(onAbortMock).toHaveBeenCalledTimes(1)
+ expect(fooSignal?.aborted).toBe(true)
+ expect(screen.queryByText('Pending...')).not.toBeInTheDocument()
+ expect(screen.queryByText('Foo page')).not.toBeInTheDocument()
+ expect(router.state.location.href).toBe('/bar')
+ expect(router.state.status).toBe('idle')
})
test('reproducer for #6388 - rapid navigation between parameterized routes should not trigger errorComponent', async () => {
@@ -912,30 +937,25 @@ test('reproducer for #6388 - rapid navigation between parameterized routes shoul
})
render( )
- await act(() => router.latestLoadPromise)
-
- const pendingComponent = screen.findByTestId('pending-component')
expect(await screen.findByTestId('home-page')).toBeInTheDocument()
-
const param1Link = await screen.findByTestId('link-to-param-1')
fireEvent.click(param1Link)
- expect(await pendingComponent).toBeInTheDocument()
+ expect(await screen.findByTestId('pending-component')).toBeInTheDocument()
const param2Link = await screen.findByTestId('link-to-param-2')
fireEvent.click(param2Link)
- expect(await pendingComponent).toBeInTheDocument()
+ expect(await screen.findByTestId('pending-component')).toBeInTheDocument()
fireEvent.click(param1Link)
- expect(await pendingComponent).toBeInTheDocument()
+ expect(await screen.findByTestId('pending-component')).toBeInTheDocument()
- await act(() => router.latestLoadPromise)
+ const paramPage = await screen.findByTestId('param-page')
expect(onAbortMock).toHaveBeenCalled()
expect(errorComponentRenderCount).not.toHaveBeenCalled()
expect(screen.queryByTestId('error-component')).not.toBeInTheDocument()
- expect(await pendingComponent).not.toBeInTheDocument()
+ expect(screen.queryByTestId('pending-component')).not.toBeInTheDocument()
- const paramPage = await screen.findByTestId('param-page')
expect(paramPage).toBeInTheDocument()
expect(paramPage).toHaveTextContent('Param Component 1 Done')
expect(loaderCompleteMock).toHaveBeenCalled()
diff --git a/packages/react-router/tests/not-found.test.tsx b/packages/react-router/tests/not-found.test.tsx
index 3754785d35..f27057c7ab 100644
--- a/packages/react-router/tests/not-found.test.tsx
+++ b/packages/react-router/tests/not-found.test.tsx
@@ -1,17 +1,24 @@
import { afterEach, beforeEach, expect, test } from 'vitest'
-import { cleanup, render, screen } from '@testing-library/react'
+import { act, cleanup, render, screen } from '@testing-library/react'
import {
Link,
Outlet,
RouterProvider,
createBrowserHistory,
+ createControlledPromise,
+ createLazyRoute,
createRootRoute,
createRoute,
createRouter,
notFound,
rootRouteId,
} from '../src'
+import {
+ RouterServer,
+ createRequestHandler,
+ renderRouterToString,
+} from '../src/ssr/server'
import type { NotFoundRouteProps, RouterHistory } from '../src'
let history: RouterHistory
@@ -27,6 +34,179 @@ afterEach(() => {
cleanup()
})
+test('navigating to an actively preloaded missing URL renders the global not-found boundary', async () => {
+ const missingLoaderStarted = createControlledPromise()
+ const missingLoader = createControlledPromise()
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ notFoundComponent: () => Missing URL boundary
,
+ loader: ({ location }) => {
+ if (location.pathname === '/missing') {
+ missingLoaderStarted.resolve()
+ return missingLoader
+ }
+ return
+ },
+ shouldReload: true,
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history,
+ })
+
+ render( )
+ expect(await screen.findByText('Home')).toBeInTheDocument()
+
+ const preload = router.preloadRoute({ to: '/missing' } as any)
+ await missingLoaderStarted
+ const navigation = router.navigate({ to: '/missing' } as any)
+ missingLoader.resolve()
+ await act(() => Promise.all([preload, navigation]))
+
+ expect(screen.getByText('Missing URL boundary')).toBeInTheDocument()
+ expect(screen.queryByText('Home')).not.toBeInTheDocument()
+})
+
+test('a lazy route notFoundComponent handles an eager beforeLoad failure', async () => {
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ notFoundComponent: () => Root not found
,
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const failingRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/lazy-not-found',
+ beforeLoad: () => {
+ throw notFound()
+ },
+ }).lazy(() =>
+ Promise.resolve(
+ createLazyRoute('/lazy-not-found')({
+ notFoundComponent: () => Lazy route not found
,
+ }),
+ ),
+ )
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, failingRoute]),
+ history,
+ })
+
+ render( )
+ expect(await screen.findByText('Home')).toBeInTheDocument()
+
+ await act(() => router.navigate({ to: '/lazy-not-found' }))
+
+ expect(screen.getByText('Lazy route not found')).toBeInTheDocument()
+ expect(screen.queryByText('Root not found')).not.toBeInTheDocument()
+})
+
+test('SSR uses a lazy route notFoundComponent for an eager beforeLoad failure', async () => {
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ notFoundComponent: () => Root not found
,
+ })
+ const failingRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/lazy-not-found',
+ beforeLoad: () => {
+ throw notFound()
+ },
+ }).lazy(() =>
+ Promise.resolve(
+ createLazyRoute('/lazy-not-found')({
+ notFoundComponent: () => Lazy route not found
,
+ }),
+ ),
+ )
+ const handler = createRequestHandler({
+ request: new Request('http://localhost/lazy-not-found'),
+ createRouter: () =>
+ createRouter({
+ routeTree: rootRoute.addChildren([failingRoute]),
+ isServer: true,
+ }),
+ })
+
+ const response = await handler(({ router, responseHeaders }) =>
+ renderRouterToString({
+ router,
+ responseHeaders,
+ children: ,
+ }),
+ )
+
+ expect(response.status).toBe(404)
+ const html = await response.text()
+ expect(html).toContain('Lazy route not found')
+ expect(html).not.toContain('Root not found')
+})
+
+test.each(['client', 'server'] as const)(
+ 'a lazy child boundary handles a fuzzy URL miss on the %s',
+ async (environment) => {
+ const rootRoute = createRootRoute({
+ component: Outlet,
+ notFoundComponent: () => Root fuzzy boundary
,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ component: Outlet,
+ notFoundComponent: () => Parent fuzzy boundary
,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ }).lazy(() =>
+ Promise.resolve(
+ createLazyRoute('/parent/child')({
+ notFoundComponent: () => Lazy child fuzzy boundary
,
+ }),
+ ),
+ )
+ const routeTree = rootRoute.addChildren([
+ parentRoute.addChildren([childRoute]),
+ ])
+
+ if (environment === 'client') {
+ const router = createRouter({ routeTree, history })
+ render( )
+ await act(() => router.navigate({ to: '/parent/child/missing' as any }))
+
+ expect(screen.getByText('Lazy child fuzzy boundary')).toBeInTheDocument()
+ expect(
+ screen.queryByText('Parent fuzzy boundary'),
+ ).not.toBeInTheDocument()
+ return
+ }
+
+ const response = await createRequestHandler({
+ request: new Request('http://localhost/parent/child/missing'),
+ createRouter: () => createRouter({ routeTree, isServer: true }),
+ })(({ router, responseHeaders }) =>
+ renderRouterToString({
+ router,
+ responseHeaders,
+ children: ,
+ }),
+ )
+
+ expect(response.status).toBe(404)
+ const html = await response.text()
+ expect(html).toContain('Lazy child fuzzy boundary')
+ expect(html).not.toContain('Parent fuzzy boundary')
+ },
+)
+
test.each([
{
notFoundMode: 'fuzzy' as const,
diff --git a/packages/react-router/tests/on-rendered-same-href-state.test.tsx b/packages/react-router/tests/on-rendered-same-href-state.test.tsx
new file mode 100644
index 0000000000..76e7a0cb9d
--- /dev/null
+++ b/packages/react-router/tests/on-rendered-same-href-state.test.tsx
@@ -0,0 +1,66 @@
+import { act } from 'react'
+import { cleanup, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { createMemoryHistory } from '@tanstack/history'
+import {
+ Outlet,
+ RouterProvider,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+const testCleanups: Array<() => void> = []
+
+afterEach(() => {
+ while (testCleanups.length) {
+ testCleanups.pop()!()
+ }
+ cleanup()
+})
+
+test('onRendered fires for a same-href navigation with a new history key', async () => {
+ const rootRoute = createRootRoute({ component: () => })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.href).toBe('/')
+ })
+ const initialHistoryKey = router.state.resolvedLocation?.state.__TSR_key
+ expect(initialHistoryKey).toBeDefined()
+
+ const onRendered = vi.fn()
+ const unsubscribe = router.subscribe('onRendered', onRendered)
+ testCleanups.push(unsubscribe)
+ await act(() =>
+ router.navigate({
+ to: '/',
+ state: { sameHrefState: true } as any,
+ }),
+ )
+ await waitFor(() => expect(onRendered).toHaveBeenCalledTimes(1))
+
+ const event = onRendered.mock.calls[0]![0]
+ expect(event.fromLocation?.state.sameHrefState).toBeUndefined()
+ expect(event.toLocation.state.sameHrefState).toBe(true)
+ expect(event.fromLocation?.href).toBe('/')
+ expect(event.toLocation.href).toBe('/')
+ expect(event.hrefChanged).toBe(false)
+ expect(event.fromLocation?.state.__TSR_key).toBe(initialHistoryKey)
+ expect(event.toLocation.state.__TSR_key).toBeDefined()
+ expect(event.toLocation.state.__TSR_key).not.toBe(initialHistoryKey)
+ expect(router.state.resolvedLocation?.state.__TSR_key).toBe(
+ event.toLocation.state.__TSR_key,
+ )
+})
diff --git a/packages/react-router/tests/pending-fallback-promise-replacement.test.tsx b/packages/react-router/tests/pending-fallback-promise-replacement.test.tsx
new file mode 100644
index 0000000000..0f5ca3703d
--- /dev/null
+++ b/packages/react-router/tests/pending-fallback-promise-replacement.test.tsx
@@ -0,0 +1,138 @@
+import * as React from 'react'
+import { act } from 'react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { cleanup, render, screen } from '@testing-library/react'
+import { createControlledPromise } from '@tanstack/router-core'
+import {
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+import type { AnyRouter } from '../src'
+
+afterEach(() => {
+ vi.useRealTimers()
+ cleanup()
+})
+
+test.each(['child', 'root'] as const)(
+ 'a mounted %s pending fallback follows an overlapping load generation',
+ async (routeLevel) => {
+ const firstReload = createControlledPromise()
+ const secondReload = createControlledPromise()
+ const reloads = [firstReload, secondReload]
+ let loaderCall = 0
+
+ const routeOptions = {
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => Loading...
,
+ loader: () => {
+ const generation = ++loaderCall
+ const gate = reloads[generation - 2]
+ return gate ? gate.then(() => ({ generation })) : { generation }
+ },
+ }
+
+ const makeRouter = (): AnyRouter => {
+ if (routeLevel === 'root') {
+ const rootRoute = createRootRoute({
+ ...routeOptions,
+ component: () => (
+
+ Generation {rootRoute.useLoaderData().generation}
+
+ ),
+ })
+ return createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+ }
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+ const pageRoute = createRoute({
+ ...routeOptions,
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ component: () => (
+
+ Generation {pageRoute.useLoaderData().generation}
+
+ ),
+ })
+ return createRouter({
+ routeTree: rootRoute.addChildren([pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/page'] }),
+ })
+ }
+ const router = makeRouter()
+
+ render( )
+ expect(await screen.findByText('Generation 1')).toBeInTheDocument()
+
+ vi.useFakeTimers()
+
+ let firstInvalidation!: Promise
+ await act(async () => {
+ firstInvalidation = router.invalidate({ forcePending: true })
+ await vi.advanceTimersByTimeAsync(0)
+ })
+
+ expect(loaderCall).toBe(2)
+ expect(screen.getByTestId('pending')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(25)
+ })
+
+ let secondInvalidation!: Promise
+ await act(async () => {
+ secondInvalidation = router.invalidate({ forcePending: true })
+ await vi.advanceTimersByTimeAsync(0)
+ })
+
+ expect(loaderCall).toBe(3)
+
+ await act(async () => {
+ firstReload.resolve()
+ await Promise.resolve()
+ })
+
+ // Completing the superseded generation cannot release the currently
+ // mounted fallback or restore its stale loader data.
+ expect(screen.getByTestId('pending')).toBeInTheDocument()
+ expect(screen.queryByText('Generation 2')).not.toBeInTheDocument()
+
+ let secondSettled = false
+ void secondInvalidation.then(() => {
+ secondSettled = true
+ })
+ await act(async () => {
+ secondReload.resolve()
+ await Promise.resolve()
+ })
+
+ expect(secondSettled).toBe(false)
+ expect(screen.getByTestId('pending')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(74)
+ })
+ expect(secondSettled).toBe(false)
+ expect(screen.getByTestId('pending')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ await Promise.all([firstInvalidation, secondInvalidation])
+ })
+
+ expect(screen.getByText('Generation 3')).toBeInTheDocument()
+ expect(screen.queryByTestId('pending')).not.toBeInTheDocument()
+ },
+)
diff --git a/packages/react-router/tests/preloaded-mount-resolution.test.tsx b/packages/react-router/tests/preloaded-mount-resolution.test.tsx
new file mode 100644
index 0000000000..1679546c79
--- /dev/null
+++ b/packages/react-router/tests/preloaded-mount-resolution.test.tsx
@@ -0,0 +1,155 @@
+import { afterEach, beforeEach, expect, onTestFinished, test, vi } from 'vitest'
+import * as React from 'react'
+import { createRoot } from 'react-dom/client'
+import { createMemoryHistory } from '@tanstack/history'
+import {
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+/**
+ * A load that settles before RouterProvider mounts (or completes within the
+ * mount effect's batch) gives the Transitioner no isLoading flip to observe.
+ * The router status must still resolve to 'idle' with resolvedLocation set,
+ * and onRendered must fire — otherwise consumers waiting on those signals
+ * deadlock forever (this hung the memory-client benchmark for 6 hours).
+ *
+ * Uses a raw createRoot without the act() test environment: act-driven
+ * flushing re-renders between the isLoading toggles and masks the race.
+ *
+ * Note: vitest's jsdom scheduler still observes the flip more often than the
+ * benchmark's environment, so this test pins the CONTRACT; the deterministic
+ * regression guard for the original hang is the memory-client:react
+ * benchmark (benchmarks/memory/client/scenarios/mount-unmount), which CI runs.
+ */
+
+let prevActEnv: unknown
+
+beforeEach(() => {
+ prevActEnv = (globalThis as any).IS_REACT_ACT_ENVIRONMENT
+ ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false
+})
+
+afterEach(() => {
+ ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = prevActEnv
+})
+
+test('mounting after a settled load still resolves status and fires onRendered', async () => {
+ const lifecycle: Array<'layout' | 'rendered'> = []
+ const Home = () => {
+ React.useLayoutEffect(() => {
+ lifecycle.push('layout')
+ }, [])
+ return Home
+ }
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: () => 'home data',
+ component: Home,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ let resolveRendered!: () => void
+ const rendered = new Promise((resolve) => {
+ resolveRendered = resolve
+ })
+ const onRendered = vi.fn(() => {
+ lifecycle.push('rendered')
+ resolveRendered()
+ })
+ const onResolved = vi.fn()
+ const onLoad = vi.fn()
+ const unsubscribers = [
+ router.subscribe('onRendered', onRendered),
+ router.subscribe('onResolved', onResolved),
+ router.subscribe('onLoad', onLoad),
+ ]
+ const unsubscribe = () => unsubscribers.forEach((fn) => fn())
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const reactRoot = createRoot(container)
+ let renderedTimeout: ReturnType | undefined
+
+ onTestFinished(() => {
+ clearTimeout(renderedTimeout)
+ unsubscribe()
+ reactRoot.unmount()
+ container.remove()
+ })
+
+ // Load fully settles before the provider mounts — the exact shape of the
+ // memory benchmark's mount/unmount cycle.
+ await router.load()
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/')
+ expect(onLoad).toHaveBeenCalledTimes(1)
+ expect(onResolved).toHaveBeenCalledTimes(1)
+ expect(onRendered).not.toHaveBeenCalled()
+
+ reactRoot.render( )
+ await Promise.race([
+ rendered,
+ new Promise((_, reject) => {
+ renderedTimeout = setTimeout(() => {
+ reject(new Error('Timed out waiting for onRendered'))
+ }, 2000)
+ }),
+ ])
+
+ expect(container.querySelector('[data-testid="home"]')).not.toBeNull()
+ expect(onRendered).toHaveBeenCalledTimes(1)
+ expect(lifecycle).toEqual(['layout', 'rendered'])
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/')
+})
+
+test('mounting during a load keeps the existing generation', async () => {
+ const gate = createControlledPromise()
+ const beforeLoad = vi.fn()
+ const loader = vi.fn(() => gate)
+ const rootRoute = createRootRoute({ component: () => })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ beforeLoad,
+ loader,
+ component: () => Home
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+ const load = router.load()
+ await vi.waitFor(() => expect(loader).toHaveBeenCalledOnce())
+
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const reactRoot = createRoot(container)
+
+ onTestFinished(() => {
+ reactRoot.unmount()
+ container.remove()
+ })
+
+ reactRoot.render( )
+ await vi.waitFor(() => expect(beforeLoad).toHaveBeenCalledOnce())
+ gate.resolve()
+ await load
+ await vi.waitFor(() => {
+ expect(container.querySelector('[data-testid="home"]')).not.toBeNull()
+ })
+
+ expect(beforeLoad).toHaveBeenCalledOnce()
+ expect(loader).toHaveBeenCalledOnce()
+})
diff --git a/packages/react-router/tests/public-presentation-lane-contract.test.tsx b/packages/react-router/tests/public-presentation-lane-contract.test.tsx
new file mode 100644
index 0000000000..862fffc93f
--- /dev/null
+++ b/packages/react-router/tests/public-presentation-lane-contract.test.tsx
@@ -0,0 +1,244 @@
+import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, describe, expect, test, vi } from 'vitest'
+import { createControlledPromise } from '@tanstack/router-core'
+import {
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
+
+describe('public presentation lane contracts', () => {
+ test('visible pending UI publishes every matched route and its loading state', async () => {
+ const parentGate = createControlledPromise()
+
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ pendingMs: 0,
+ pendingComponent: () => Loading parent
,
+ loader: () => parentGate,
+ component: Outlet,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ loader: () => 'child data',
+ component: () => Child content
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ parentRoute.addChildren([childRoute]),
+ ]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Home')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ let navigation!: Promise
+ await act(async () => {
+ navigation = router.navigate({ to: '/parent/child' })
+ await Promise.resolve()
+ })
+
+ expect(await screen.findByText('Loading parent')).toBeInTheDocument()
+ expect(screen.queryByText('Child content')).not.toBeInTheDocument()
+
+ expect(router.state.matches.map((match) => match.routeId)).toEqual([
+ rootRoute.id,
+ parentRoute.id,
+ childRoute.id,
+ ])
+ expect(
+ router.state.matches.find((match) => match.routeId === parentRoute.id),
+ ).toMatchObject({ status: 'pending', isFetching: 'loader' })
+
+ await act(async () => {
+ parentGate.resolve('parent data')
+ await navigation
+ })
+
+ expect(screen.getByText('Child content')).toBeInTheDocument()
+ expect(router.state.status).toBe('idle')
+ })
+
+ test('same-boundary takeover republishes successor search without restarting pendingMinMs', async () => {
+ const firstGate = createControlledPromise()
+ const secondGate = createControlledPromise()
+
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const pageRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/page',
+ validateSearch: (search: Record) => ({
+ revision: Number(search.revision),
+ }),
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => Loading page
,
+ beforeLoad: ({ search }) =>
+ search.revision === 1 ? firstGate : secondGate,
+ component: () => {
+ const search = pageRoute.useSearch()
+ return Page revision {search.revision}
+ },
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, pageRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Home')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ vi.useFakeTimers()
+ vi.setSystemTime(0)
+
+ let successorSettled = false
+ let settledAtOriginalDeadline = false
+ let renderedAtOriginalDeadline = false
+ try {
+ await act(async () => {
+ void router.navigate({
+ to: '/page',
+ search: { revision: 1 },
+ })
+ await vi.advanceTimersByTimeAsync(0)
+ })
+ expect(screen.getByText('Loading page')).toBeInTheDocument()
+ expect(router.state.matches.at(-1)?.search).toMatchObject({ revision: 1 })
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(25)
+ })
+
+ let secondNavigation!: Promise
+ await act(async () => {
+ secondNavigation = router.navigate({
+ to: '/page',
+ search: { revision: 2 },
+ })
+ await vi.advanceTimersByTimeAsync(0)
+ })
+
+ expect(screen.getByText('Loading page')).toBeInTheDocument()
+ expect(router.state.location.search).toMatchObject({ revision: 2 })
+ expect(router.state.matches.at(-1)?.search).toMatchObject({ revision: 2 })
+
+ void secondNavigation.then(() => {
+ successorSettled = true
+ })
+ await act(async () => {
+ secondGate.resolve()
+ await Promise.resolve()
+ })
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(74)
+ })
+ expect(successorSettled).toBe(false)
+ expect(screen.getByText('Loading page')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ await Promise.resolve()
+ })
+
+ settledAtOriginalDeadline = successorSettled
+ renderedAtOriginalDeadline =
+ screen.queryByText('Page revision 2') !== null
+ } finally {
+ // Finish a faulty implementation too, so a deadline assertion cannot
+ // strand this router and contaminate the following test.
+ firstGate.resolve()
+ secondGate.resolve()
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1_000)
+ await Promise.resolve()
+ })
+ }
+
+ expect({
+ settled: settledAtOriginalDeadline,
+ rendered: renderedAtOriginalDeadline,
+ }).toEqual({ settled: true, rendered: true })
+ expect(screen.getByText('Page revision 2')).toBeInTheDocument()
+ expect(screen.queryByText('Loading page')).not.toBeInTheDocument()
+ })
+
+ test('a reentrant navigation from onResolved suppresses the stale onRendered event', async () => {
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+ const firstRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/first',
+ component: () => First
,
+ })
+ const secondRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/second',
+ component: () => Second
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, firstRoute, secondRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Home')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ const renderedPaths: Array = []
+ let successor: Promise | undefined
+ const unsubscribeResolved = router.subscribe('onResolved', (event) => {
+ if (event.toLocation.pathname === '/first') {
+ successor = router.navigate({ to: '/second' })
+ }
+ })
+ const unsubscribeRendered = router.subscribe('onRendered', (event) => {
+ if (event.toLocation.pathname !== '/') {
+ renderedPaths.push(event.toLocation.pathname)
+ }
+ })
+
+ try {
+ await act(() => router.navigate({ to: '/first' }))
+ await act(async () => {
+ await successor
+ })
+
+ expect(screen.getByText('Second')).toBeInTheDocument()
+ expect(screen.queryByText('First')).not.toBeInTheDocument()
+ expect(renderedPaths).toEqual(['/second'])
+ } finally {
+ unsubscribeResolved()
+ unsubscribeRendered()
+ }
+ })
+})
diff --git a/packages/react-router/tests/react-render-owner-contract.test.tsx b/packages/react-router/tests/react-render-owner-contract.test.tsx
new file mode 100644
index 0000000000..0797fa5264
--- /dev/null
+++ b/packages/react-router/tests/react-render-owner-contract.test.tsx
@@ -0,0 +1,101 @@
+import { act } from 'react'
+import { cleanup, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRouter,
+} from '../src'
+
+const testCleanups: Array<() => void> = []
+
+afterEach(() => {
+ while (testCleanups.length) {
+ testCleanups.pop()!()
+ }
+ cleanup()
+})
+
+test('a suspended same-membership publication cannot acknowledge its successor', async () => {
+ const firstRenderStarted = createControlledPromise()
+ const firstRenderGate = createControlledPromise()
+ let signaledFirstRender = false
+
+ const rootRoute = createRootRoute({
+ validateSearch: (search: Record) => ({
+ revision: Number(search.revision),
+ }),
+ component: () => {
+ const revision = rootRoute.useSearch().revision
+ if (revision === 1 && firstRenderGate.status === 'pending') {
+ if (!signaledFirstRender) {
+ signaledFirstRender = true
+ firstRenderStarted.resolve()
+ }
+ throw firstRenderGate
+ }
+ return Root revision {revision}
+ },
+ })
+ const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/?revision=0'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Root revision 0')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+ const initialIds = router.state.matches.map((match) => match.routeId)
+
+ const renderedRevisions: Array = []
+ const unsubscribe = router.subscribe('onRendered', (event) => {
+ renderedRevisions.push(
+ Number((event.toLocation.search as Record).revision),
+ )
+ })
+ testCleanups.push(unsubscribe)
+
+ let firstNavigation!: Promise
+ await act(async () => {
+ firstNavigation = router.navigate({
+ to: '/',
+ search: { revision: 1 },
+ })
+ await firstRenderStarted
+ })
+
+ const firstSettled = vi.fn()
+ void firstNavigation.then(firstSettled)
+ expect(router.state.matches.map((match) => match.routeId)).toEqual(initialIds)
+ expect(router.state.matches[0]?.search.revision).toBe(1)
+ expect(screen.getByText('Root revision 0')).toBeInTheDocument()
+ expect(firstSettled).not.toHaveBeenCalled()
+ expect(renderedRevisions).toEqual([])
+
+ try {
+ await act(() =>
+ router.navigate({
+ to: '/',
+ search: { revision: 2 },
+ }),
+ )
+ await firstNavigation
+
+ expect(screen.getByText('Root revision 2')).toBeInTheDocument()
+ expect(router.state.matches.map((match) => match.routeId)).toEqual(
+ initialIds,
+ )
+ expect(renderedRevisions).toEqual([2])
+ expect(firstSettled).toHaveBeenCalledOnce()
+ } finally {
+ await act(async () => {
+ firstRenderGate.resolve()
+ await Promise.resolve()
+ })
+ }
+
+ expect(screen.getByText('Root revision 2')).toBeInTheDocument()
+ expect(renderedRevisions).toEqual([2])
+})
diff --git a/packages/react-router/tests/redirect-chain-first-load.test.tsx b/packages/react-router/tests/redirect-chain-first-load.test.tsx
new file mode 100644
index 0000000000..b3f4618a28
--- /dev/null
+++ b/packages/react-router/tests/redirect-chain-first-load.test.tsx
@@ -0,0 +1,110 @@
+import * as React from 'react'
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+
+import {
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ redirect,
+} from '../src'
+import { sleep } from './utils'
+
+afterEach(() => {
+ vi.restoreAllMocks()
+ cleanup()
+})
+
+// A chain of async layout beforeLoad redirects during the very first load
+// (search-stripping self-redirect -> layout redirect -> child redirect) used
+// to leave a match rendering with a nulled loadPromise, crashing
+// MatchInnerImpl with an uncaught `undefined`. Pending UI is enabled for
+// every match (defaultPendingMs: 0) to force pending publication mid-chain.
+// The production auto-code-splitting reproduction for issue #7457 lives in
+// e2e/react-router/issue-7457.
+test('chained layout beforeLoad redirects on first load render the final target without throwing from MatchInner', async () => {
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ const rootRoute = createRootRoute({ component: () => })
+
+ const userLayout = createRoute({
+ id: 'user',
+ getParentRoute: () => rootRoute,
+ validateSearch: (search: Record): { flag?: boolean } => ({
+ flag: search.flag === true || search.flag === 'true' ? true : undefined,
+ }),
+ beforeLoad: async ({ search }) => {
+ if (search.flag) {
+ await sleep(20)
+ throw redirect({
+ to: '.',
+ replace: true,
+ search: (prev: any) => ({ ...prev, flag: undefined }),
+ })
+ }
+ },
+ component: () => ,
+ })
+
+ const dashboardLayout = createRoute({
+ id: 'dashboard',
+ getParentRoute: () => userLayout,
+ beforeLoad: async () => {
+ await sleep(30)
+ throw redirect({ to: '/intro', replace: true })
+ },
+ component: () => ,
+ })
+
+ const homeRoute = createRoute({
+ getParentRoute: () => dashboardLayout,
+ path: '/home',
+ component: () => Home
,
+ })
+
+ const introLayout = createRoute({
+ getParentRoute: () => userLayout,
+ path: '/intro',
+ beforeLoad: ({ location }) => {
+ if (location.pathname !== '/intro/step') {
+ throw redirect({ to: '/intro/step', replace: true })
+ }
+ },
+ component: () => ,
+ })
+
+ const introIndexRoute = createRoute({
+ getParentRoute: () => introLayout,
+ path: '/',
+ component: () => Intro index
,
+ })
+
+ const introStepRoute = createRoute({
+ getParentRoute: () => introLayout,
+ path: 'step',
+ component: () => Intro step
,
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ userLayout.addChildren([
+ dashboardLayout.addChildren([homeRoute]),
+ introLayout.addChildren([introIndexRoute, introStepRoute]),
+ ]),
+ ]),
+ defaultPendingMs: 0,
+ defaultPendingComponent: () => loading
,
+ history: createMemoryHistory({ initialEntries: ['/home?flag=true'] }),
+ })
+
+ render( )
+
+ expect(
+ await screen.findByTestId('intro-step', undefined, { timeout: 5_000 }),
+ ).toBeInTheDocument()
+ expect(router.state.location.pathname).toBe('/intro/step')
+ expect(consoleError).not.toHaveBeenCalled()
+})
diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx
index cc15f0da36..0c254fb098 100644
--- a/packages/react-router/tests/redirect.test.tsx
+++ b/packages/react-router/tests/redirect.test.tsx
@@ -1,5 +1,6 @@
import * as React from 'react'
import {
+ act,
cleanup,
configure,
fireEvent,
@@ -8,13 +9,13 @@ import {
} from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
+import { createControlledPromise } from '@tanstack/router-core'
import {
Link,
Outlet,
RouterProvider,
createBrowserHistory,
- createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
@@ -45,6 +46,114 @@ const WAIT_TIME = 100
describe('redirect', () => {
describe('SPA', () => {
configure({ reactStrictMode: true })
+
+ test('allows a same-location redirect to settle after a side effect', async () => {
+ let firstLoad = true
+ const loader = vi.fn(() => {
+ if (firstLoad) {
+ firstLoad = false
+ throw redirect({ to: '/' })
+ }
+ })
+ const rootRoute = createRootRoute()
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader,
+ component: () => Index page
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history,
+ })
+
+ render( )
+
+ expect(await screen.findByText('Index page')).toBeInTheDocument()
+ expect(window.location.pathname).toBe('/')
+ expect(loader).toHaveBeenCalledTimes(2)
+ expect(router.state.status).toBe('idle')
+ })
+
+ test('renders a root error after too many same-location redirects', async () => {
+ const loader = vi.fn(() => {
+ throw redirect({ to: '/' })
+ })
+ const rootRoute = createRootRoute({
+ errorComponent: ({ error }) => (
+ Root: {error.message}
+ ),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader,
+ errorComponent: ({ error }) => (
+ Index: {error.message}
+ ),
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history,
+ })
+
+ render( )
+
+ expect(await screen.findByTestId('root-error')).toHaveTextContent(
+ 'Root: Too many redirects',
+ )
+ expect(screen.queryByTestId('index-error')).not.toBeInTheDocument()
+ expect(window.location.pathname).toBe('/')
+ expect(loader).toHaveBeenCalledTimes(21)
+ expect(router.state.status).toBe('idle')
+ })
+
+ test('renders a root error after too many alternating redirects', async () => {
+ const indexLoader = vi.fn(() => {
+ throw redirect({ to: '/other' })
+ })
+ const otherLoader = vi.fn(() => {
+ throw redirect({ to: '/' })
+ })
+ const rootRoute = createRootRoute({
+ errorComponent: ({ error }) => (
+ Root: {error.message}
+ ),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: indexLoader,
+ errorComponent: ({ error }) => (
+ Index: {error.message}
+ ),
+ })
+ const otherRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/other',
+ loader: otherLoader,
+ errorComponent: ({ error }) => (
+ Other: {error.message}
+ ),
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, otherRoute]),
+ history,
+ })
+
+ render( )
+
+ expect(await screen.findByTestId('root-error')).toHaveTextContent(
+ 'Root: Too many redirects',
+ )
+ expect(screen.queryByTestId('index-error')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('other-error')).not.toBeInTheDocument()
+ expect(window.location.pathname).toBe('/')
+ expect(indexLoader).toHaveBeenCalledTimes(11)
+ expect(otherLoader).toHaveBeenCalledTimes(10)
+ expect(router.state.status).toBe('idle')
+ })
+
test('when `redirect` is thrown in `beforeLoad`', async () => {
const nestedLoaderMock = vi.fn()
const nestedFooLoaderMock = vi.fn()
@@ -115,6 +224,7 @@ describe('redirect', () => {
test('when root `beforeLoad` redirects while root pendingComponent is showing and the target route is lazy', async () => {
let hasRedirected = false
+ const beforeLoad = createControlledPromise()
const consoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {})
@@ -124,7 +234,7 @@ describe('redirect', () => {
pendingMs: 0,
pendingComponent: () => loading
,
beforeLoad: async () => {
- await sleep(WAIT_TIME)
+ await beforeLoad
if (!hasRedirected) {
hasRedirected = true
throw redirect({ to: '/posts' })
@@ -150,6 +260,14 @@ describe('redirect', () => {
render( )
+ try {
+ expect(await screen.findByTestId('pending')).toBeInTheDocument()
+ } finally {
+ await act(() => {
+ beforeLoad.resolve()
+ })
+ }
+
// The lazy target route adds the async boundary that exposes the stale
// redirected-match render path this regression is guarding.
expect(await screen.findByTestId('lazy-route-page')).toBeInTheDocument()
@@ -311,116 +429,4 @@ describe('redirect', () => {
expect(window.location.pathname).toBe('/final')
})
})
-
- describe('SSR', () => {
- test('when `redirect` is thrown in `beforeLoad`', async () => {
- const rootRoute = createRootRoute()
-
- const indexRoute = createRoute({
- path: '/',
- getParentRoute: () => rootRoute,
- beforeLoad: () => {
- throw redirect({
- to: '/about',
- })
- },
- })
-
- const aboutRoute = createRoute({
- path: '/about',
- getParentRoute: () => rootRoute,
- component: () => {
- return 'About'
- },
- })
-
- const router = createRouter({
- routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
- // Mock server mode
- isServer: true,
- history: createMemoryHistory({
- initialEntries: ['/'],
- }),
- })
-
- await router.load()
-
- expect(router.state.redirect).toBeDefined()
- expect(router.state.redirect).toBeInstanceOf(Response)
- const redirectResponse = router.state.redirect!
-
- expect(redirectResponse.options).toEqual({
- _fromLocation: expect.objectContaining({
- hash: '',
- href: '/',
- pathname: '/',
- search: {},
- searchStr: '',
- }),
- to: '/about',
- href: '/about',
- statusCode: 307,
- })
- })
-
- test('when `redirect` is thrown in `loader`', async () => {
- const rootRoute = createRootRoute()
-
- const indexRoute = createRoute({
- path: '/',
- getParentRoute: () => rootRoute,
- loader: () => {
- throw redirect({
- to: '/about',
- })
- },
- })
-
- const aboutRoute = createRoute({
- path: '/about',
- getParentRoute: () => rootRoute,
- component: () => {
- return 'About'
- },
- })
-
- const router = createRouter({
- history: createMemoryHistory({
- initialEntries: ['/'],
- }),
- routeTree: rootRoute.addChildren([indexRoute, aboutRoute]),
- // Mock server mode
- isServer: true,
- })
-
- await router.load()
-
- const currentRedirect = router.state.redirect
-
- expect(currentRedirect).toBeDefined()
- expect(currentRedirect).toBeInstanceOf(Response)
- const redirectResponse = currentRedirect!
- expect(redirectResponse.status).toEqual(307)
- expect(redirectResponse.headers.get('Location')).toEqual('/about')
- expect(redirectResponse.options).toEqual({
- _fromLocation: {
- external: false,
- hash: '',
- href: '/',
- publicHref: '/',
- pathname: '/',
- search: {},
- searchStr: '',
- state: {
- __TSR_index: 0,
- __TSR_key: redirectResponse.options._fromLocation!.state.__TSR_key,
- key: redirectResponse.options._fromLocation!.state.key,
- },
- },
- href: '/about',
- to: '/about',
- statusCode: 307,
- })
- })
- })
})
diff --git a/packages/react-router/tests/renderRouterToStream.test.tsx b/packages/react-router/tests/renderRouterToStream.test.tsx
index 74e180a552..f42456b7be 100644
--- a/packages/react-router/tests/renderRouterToStream.test.tsx
+++ b/packages/react-router/tests/renderRouterToStream.test.tsx
@@ -1,4 +1,4 @@
-import { afterEach, describe, expect, test, vi } from 'vitest'
+import { afterEach, describe, expect, onTestFinished, test, vi } from 'vitest'
import { attachRouterServerSsrUtils } from '@tanstack/router-core/ssr/server'
import { createMemoryHistory, createRootRoute, createRouter } from '../src'
@@ -52,6 +52,35 @@ function unwrapResponse(
}
describe('renderRouterToStream - pipeable sync errors', () => {
+ test('request abort cancels readable rendering without consuming the response body', async () => {
+ const cancel = vi.fn()
+ const stream = Object.assign(new ReadableStream({ cancel }), {
+ allReady: Promise.resolve(),
+ })
+ reactDomServerMocks.renderToReadableStream = vi.fn(() => stream)
+
+ const router = await buildRouter()
+ const controller = new AbortController()
+ onTestFinished(() => {
+ router.serverSsr?.cleanup()
+ })
+
+ const response = unwrapResponse(
+ await renderRouterToStream({
+ request: new Request('http://localhost/', {
+ signal: controller.signal,
+ }),
+ router,
+ responseHeaders: new Headers(),
+ children: null,
+ }),
+ )
+
+ expect(response.body).not.toBeNull()
+ controller.abort(new Error('request-gone'))
+ await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce())
+ })
+
test('sync onError before pipeable is assigned still aborts pipeable', async () => {
const abort = vi.fn()
reactDomServerMocks.renderToPipeableStream.mockImplementationOnce(
@@ -197,7 +226,7 @@ describe('renderRouterToStream - pipeable sync errors', () => {
}
})
- test('request abort aborts pipeable and errors body', async () => {
+ test('request abort cancels pipeable rendering before the response body is consumed', async () => {
const abort = vi.fn()
reactDomServerMocks.renderToPipeableStream.mockImplementationOnce(
(_children, opts) => {
@@ -220,13 +249,14 @@ describe('renderRouterToStream - pipeable sync errors', () => {
}),
)
+ expect(response.body).not.toBeNull()
controller.abort(new Error('request-gone'))
+ await vi.waitFor(() => expect(abort).toHaveBeenCalledOnce())
const terminated = await Promise.race([
expectBodyRejects(response, 'request-gone').then(() => true),
new Promise((resolve) => setTimeout(() => resolve(false), 2000)),
])
expect(terminated).toBe(true)
- expect(abort).toHaveBeenCalledOnce()
} finally {
router.serverSsr?.cleanup()
}
diff --git a/packages/react-router/tests/root-pending-min.test.tsx b/packages/react-router/tests/root-pending-min.test.tsx
new file mode 100644
index 0000000000..0216ae4fec
--- /dev/null
+++ b/packages/react-router/tests/root-pending-min.test.tsx
@@ -0,0 +1,186 @@
+import * as React from 'react'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import { hydrateRoot } from 'react-dom/client'
+import { renderToString } from 'react-dom/server'
+import { afterEach, expect, test, vi } from 'vitest'
+import { dehydrateSsrMatchId } from '../../router-core/src/ssr/ssr-match-id'
+import { hydrate } from '../src/ssr/client'
+import {
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRouter,
+} from '../src'
+
+const testCleanups: Array<() => void | Promise> = []
+
+afterEach(async () => {
+ while (testCleanups.length) {
+ await testCleanups.pop()!()
+ }
+ cleanup()
+ vi.useRealTimers()
+ delete window.$_TSR
+})
+
+test('a post-hydration root reload keeps its fallback through pendingMinMs', async () => {
+ const reloadGate = createControlledPromise()
+ const rootLoader = vi.fn(() => reloadGate.then(() => ({ generation: 2 })))
+ const rootRoute = createRootRoute({
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => Pending
,
+ loader: rootLoader,
+ component: () => (
+
+ Generation {rootRoute.useLoaderData().generation}
+
+ ),
+ })
+ const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+ const rootMatch = router.matchRoutes(router.latestLocation)[0]!
+ // This is the same public bootstrap shape produced by the server. Calling
+ // hydrate() ensures router.ssr and the active match are established through
+ // the real client hydration path rather than by mutating router stores.
+ window.$_TSR = {
+ router: {
+ manifest: { routes: {} },
+ dehydratedData: {},
+ matches: [
+ {
+ i: dehydrateSsrMatchId(rootMatch.id),
+ s: 'success',
+ ssr: true,
+ l: { generation: 1 },
+ u: Date.now(),
+ },
+ ],
+ },
+ h: vi.fn(),
+ e: vi.fn(),
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(router)
+ expect(router.ssr).toBeDefined()
+
+ render( )
+ expect(screen.getByTestId('root-content')).toHaveTextContent('Generation 1')
+ expect(rootLoader).not.toHaveBeenCalled()
+
+ vi.useFakeTimers()
+
+ let invalidation!: Promise
+ await act(async () => {
+ invalidation = router.invalidate({ forcePending: true })
+ await vi.advanceTimersByTimeAsync(0)
+ })
+
+ expect(rootLoader).toHaveBeenCalledTimes(1)
+ expect(screen.getByTestId('root-pending')).toBeInTheDocument()
+ expect(screen.getByTestId('root-content')).not.toBeVisible()
+
+ await act(async () => {
+ reloadGate.resolve()
+ await Promise.resolve()
+ })
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(99)
+ })
+ expect(screen.getByTestId('root-pending')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ await invalidation
+ })
+ expect(screen.queryByTestId('root-pending')).not.toBeInTheDocument()
+ expect(screen.getByTestId('root-content')).toHaveTextContent('Generation 2')
+})
+
+test('root route hydration preserves component state across its Suspense boundary', async () => {
+ const mounts = vi.fn()
+ const unmounts = vi.fn()
+ const initializers = vi.fn(() => 'preserved')
+
+ const rootRoute = createRootRoute({
+ pendingComponent: () => Root pending
,
+ component: function RootComponent() {
+ const [value] = React.useState(initializers)
+ React.useEffect(() => {
+ mounts()
+ return unmounts
+ }, [])
+ return {value}
+ },
+ })
+ const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+ await router.load()
+
+ // Model the server render and the client router produced by hydrate(). The
+ // outer root boundary is intentionally absent from both trees, while the
+ // route's own pending boundary is present in both.
+ router.ssr = { manifest: { routes: {} } }
+ router.isServer = true
+ const html = renderToString( )
+ router.isServer = false
+ expect(html).toContain('')
+ expect(html).toContain('preserved')
+
+ const container = document.createElement('div')
+ container.innerHTML = html
+ document.body.appendChild(container)
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
+ let root!: ReturnType
+ await act(async () => {
+ root = hydrateRoot(container, )
+ testCleanups.push(async () => {
+ await act(() => root.unmount())
+ consoleError.mockRestore()
+ container.remove()
+ })
+ await Promise.resolve()
+ })
+
+ expect(container).toHaveTextContent('preserved')
+ // One initializer belongs to the server render and one to client
+ // hydration. The stable boundary must preserve that hydrated client
+ // instance instead of creating a third one.
+ expect(initializers).toHaveBeenCalledTimes(2)
+ expect(mounts).toHaveBeenCalledTimes(1)
+ expect(unmounts).not.toHaveBeenCalled()
+ expect(consoleError).not.toHaveBeenCalled()
+})
+
+test('server rendering uses the root pending boundary for route component suspension', async () => {
+ const gate = createControlledPromise()
+ const rootRoute = createRootRoute({
+ pendingComponent: () => Server root pending
,
+ component: () => {
+ throw gate
+ },
+ })
+ const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+ router.isServer = true
+ await router.load()
+
+ const html = renderToString( )
+
+ // renderToString cannot wait for Suspense, but the root route's stable
+ // boundary contains the suspension and emits its fallback. Streaming SSR
+ // can wait for the same boundary instead.
+ expect(html).toContain('Server root pending')
+})
diff --git a/packages/react-router/tests/routeContext.test.tsx b/packages/react-router/tests/routeContext.test.tsx
index d11f86420e..e00228e280 100644
--- a/packages/react-router/tests/routeContext.test.tsx
+++ b/packages/react-router/tests/routeContext.test.tsx
@@ -141,7 +141,7 @@ describe('context function', () => {
})
test('when loader deps change', async () => {
- const mockContextFn = vi.fn()
+ let generation = 0
const rootRoute = createRootRoute()
const indexRoute = createRoute({
@@ -153,14 +153,21 @@ describe('context function', () => {
path: '/',
loaderDeps: ({ search }) => ({ foo: search.foo }),
context: ({ deps }) => {
- mockContextFn(deps)
+ return {
+ generation: ++generation,
+ deps: JSON.stringify(deps),
+ }
},
component: () => {
const navigate = indexRoute.useNavigate()
+ const context = indexRoute.useRouteContext()
return (
Index page
search: {JSON.stringify(indexRoute.useSearch())}
+
+ context: {context.generation}:{context.deps}
+
{
navigate({ search: (p: any) => ({ ...p, foo: 'foo-1' }) })
@@ -208,44 +215,37 @@ describe('context function', () => {
await findByText('Index page')
await findByText(`search: ${JSON.stringify({})}`)
-
- expect(mockContextFn).toHaveBeenCalledOnce()
- expect(mockContextFn).toHaveBeenCalledWith({})
- mockContextFn.mockClear()
+ await findByText('context: 1:{}')
await clickButton('foo-1')
await findByText(`search: ${JSON.stringify({ foo: 'foo-1' })}`)
- expect(mockContextFn).toHaveBeenCalledOnce()
- expect(mockContextFn).toHaveBeenCalledWith({ foo: 'foo-1' })
+ await findByText('context: 2:{"foo":"foo-1"}')
- mockContextFn.mockClear()
await clickButton('foo-1')
await findByText(`search: ${JSON.stringify({ foo: 'foo-1' })}`)
- expect(mockContextFn).not.toHaveBeenCalled()
+ await findByText('context: 2:{"foo":"foo-1"}')
await clickButton('bar-1')
await findByText(
`search: ${JSON.stringify({ foo: 'foo-1', bar: 'bar-1' })}`,
)
- expect(mockContextFn).not.toHaveBeenCalled()
+ await findByText('context: 2:{"foo":"foo-1"}')
await clickButton('foo-2')
await findByText(
`search: ${JSON.stringify({ foo: 'foo-2', bar: 'bar-1' })}`,
)
- expect(mockContextFn).toHaveBeenCalledWith({ foo: 'foo-2' })
- mockContextFn.mockClear()
+ await findByText('context: 3:{"foo":"foo-2"}')
await clickButton('bar-2')
await findByText(
`search: ${JSON.stringify({ foo: 'foo-2', bar: 'bar-2' })}`,
)
- expect(mockContextFn).not.toHaveBeenCalled()
+ await findByText('context: 3:{"foo":"foo-2"}')
await clickButton('clear')
await findByText(`search: ${JSON.stringify({})}`)
- expect(mockContextFn).toHaveBeenCalledOnce()
- expect(mockContextFn).toHaveBeenCalledWith({})
+ await findByText('context: 4:{}')
})
})
diff --git a/packages/react-router/tests/router-client-stream-cleanup.test.tsx b/packages/react-router/tests/router-client-stream-cleanup.test.tsx
new file mode 100644
index 0000000000..865b7cd732
--- /dev/null
+++ b/packages/react-router/tests/router-client-stream-cleanup.test.tsx
@@ -0,0 +1,55 @@
+import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { Component } from 'react'
+import { createMemoryHistory } from '@tanstack/history'
+import { RouterClient } from '../src/ssr/RouterClient'
+import { createRootRoute, createRouter } from '../src'
+import type { ReactNode } from 'react'
+
+const hydrate = vi.hoisted(() => vi.fn())
+
+vi.mock('@tanstack/router-core/ssr/client', () => ({ hydrate }))
+
+class ErrorBoundary extends Component<
+ { children: ReactNode },
+ { error?: Error }
+> {
+ state: { error?: Error } = {}
+
+ static getDerivedStateFromError(error: Error) {
+ return { error }
+ }
+
+ render() {
+ return this.state.error ? this.state.error.message : this.props.children
+ }
+}
+
+afterEach(() => {
+ cleanup()
+ delete window.$_TSR
+ hydrate.mockReset()
+})
+
+test('RouterClient signals streaming cleanup without hiding a hydration failure', async () => {
+ const error = new Error('hydration failed')
+ hydrate.mockRejectedValue(error)
+ const rootRoute = createRootRoute({ component: () => Ready
})
+ const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+ const hydrated = vi.fn()
+ window.$_TSR = { h: hydrated } as any
+
+ await act(async () => {
+ render(
+
+
+ ,
+ )
+ })
+
+ await waitFor(() => expect(hydrated).toHaveBeenCalledTimes(1))
+ expect(await screen.findByText(error.message)).toBeInTheDocument()
+})
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index cf3b1c11fe..43c4d47423 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -8,7 +8,11 @@ import {
waitFor,
} from '@testing-library/react'
import { z } from 'zod'
-import { composeRewrites, notFound } from '@tanstack/router-core'
+import {
+ composeRewrites,
+ createControlledPromise,
+ notFound,
+} from '@tanstack/router-core'
import {
Link,
Outlet,
@@ -2205,22 +2209,26 @@ describe('does not strip search params if search validation fails', () => {
})
})
-describe('statusCode', () => {
- it('should reset statusCode to 200 when navigating from 404 to valid route', async () => {
+describe('navigation outcomes', () => {
+ it('should recover from a not-found route when navigating to a valid route', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
- const rootRoute = createRootRoute()
+ const rootRoute = createRootRoute({
+ notFoundComponent: () => (
+ Not Found
+ ),
+ })
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
- component: () => Home
,
+ component: () => Home
,
})
const validRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/valid',
- component: () => Valid Route
,
+ component: () => Valid Route
,
})
const routeTree = rootRoute.addChildren([indexRoute, validRoute])
@@ -2228,27 +2236,28 @@ describe('statusCode', () => {
render( )
- expect(router.state.statusCode).toBe(200)
-
await act(() => router.navigate({ to: '/' }))
- expect(router.state.statusCode).toBe(200)
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
await act(() => router.navigate({ to: '/non-existing' }))
- expect(router.state.statusCode).toBe(404)
+ expect(await screen.findByTestId('root-not-found')).toBeInTheDocument()
+ expect(screen.queryByTestId('home-page')).not.toBeInTheDocument()
await act(() => router.navigate({ to: '/valid' }))
- expect(router.state.statusCode).toBe(200)
+ expect(await screen.findByTestId('valid-page')).toBeInTheDocument()
+ expect(screen.queryByTestId('root-not-found')).not.toBeInTheDocument()
await act(() => router.navigate({ to: '/another-non-existing' }))
- expect(router.state.statusCode).toBe(404)
+ expect(await screen.findByTestId('root-not-found')).toBeInTheDocument()
+ expect(screen.queryByTestId('valid-page')).not.toBeInTheDocument()
})
describe.each([true, false])(
- 'status code is set when loader/beforeLoad throws (isAsync=%s)',
+ 'loader and beforeLoad outcomes are rendered (isAsync=%s)',
async (isAsync) => {
const throwingFun = isAsync
? (toThrow: () => void) => async () => {
- await new Promise((resolve) => setTimeout(resolve, 10))
+ await Promise.resolve()
toThrow()
}
: (toThrow: () => void) => toThrow
@@ -2259,15 +2268,19 @@ describe('statusCode', () => {
const throwError = throwingFun(() => {
throw new Error('test-error')
})
- it('should set statusCode to 404 when a route loader throws a notFound()', async () => {
+ it('should render notFoundComponent when a route loader throws a notFound()', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
- const rootRoute = createRootRoute()
+ const rootRoute = createRootRoute({
+ notFoundComponent: () => (
+ Root Not Found
+ ),
+ })
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
- component: () => Home
,
+ component: () => Home
,
})
const loaderThrowsRoute = createRoute({
@@ -2278,7 +2291,7 @@ describe('statusCode', () => {
loader will throw
),
notFoundComponent: () => (
- Not Found
+ Route Not Found
),
})
@@ -2287,29 +2300,27 @@ describe('statusCode', () => {
render( )
- expect(router.state.statusCode).toBe(200)
-
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
await act(() => router.navigate({ to: '/loader-throws-not-found' }))
- expect(router.state.statusCode).toBe(404)
- expect(
- await screen.findByTestId('not-found-component'),
- ).toBeInTheDocument()
+ expect(await screen.findByTestId('route-not-found')).toBeInTheDocument()
+ expect(screen.queryByTestId('root-not-found')).not.toBeInTheDocument()
expect(screen.queryByTestId('route-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('home-page')).not.toBeInTheDocument()
})
- it('should set statusCode to 404 when a route beforeLoad throws a notFound()', async () => {
+ it('should render notFoundComponent when a route beforeLoad throws a notFound()', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute({
notFoundComponent: () => (
- Not Found
+ Root Not Found
),
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
- component: () => Home
,
+ component: () => Home
,
})
const beforeLoadThrowsRoute = createRoute({
@@ -2320,7 +2331,7 @@ describe('statusCode', () => {
beforeLoad will throw
),
notFoundComponent: () => (
- Not Found
+ Route Not Found
),
})
@@ -2332,17 +2343,15 @@ describe('statusCode', () => {
render( )
- expect(router.state.statusCode).toBe(200)
-
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
await act(() => router.navigate({ to: '/beforeload-throws-not-found' }))
- expect(router.state.statusCode).toBe(404)
- expect(
- await screen.findByTestId('not-found-component'),
- ).toBeInTheDocument()
+ expect(await screen.findByTestId('route-not-found')).toBeInTheDocument()
+ expect(screen.queryByTestId('root-not-found')).not.toBeInTheDocument()
expect(screen.queryByTestId('route-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('home-page')).not.toBeInTheDocument()
})
- it('should set statusCode to 500 when a route loader throws an Error', async () => {
+ it('should render errorComponent when a route loader throws an Error', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute()
@@ -2368,15 +2377,12 @@ describe('statusCode', () => {
render( )
- expect(router.state.statusCode).toBe(200)
-
await act(() => router.navigate({ to: '/loader-throws-error' }))
- expect(router.state.statusCode).toBe(500)
expect(await screen.findByTestId('error-component')).toBeInTheDocument()
expect(screen.queryByTestId('route-component')).not.toBeInTheDocument()
})
- it('should set statusCode to 500 when a route beforeLoad throws an Error', async () => {
+ it('should render errorComponent when a route beforeLoad throws an Error', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute()
@@ -2405,10 +2411,7 @@ describe('statusCode', () => {
render( )
- expect(router.state.statusCode).toBe(200)
-
await act(() => router.navigate({ to: '/beforeload-throws-error' }))
- expect(router.state.statusCode).toBe(500)
expect(await screen.findByTestId('error-component')).toBeInTheDocument()
expect(screen.queryByTestId('route-component')).not.toBeInTheDocument()
})
@@ -2417,7 +2420,7 @@ describe('statusCode', () => {
})
describe('notFound in beforeLoad with pendingComponent', () => {
- it('should transition router.state.status to idle when child beforeLoad throws notFound and parent has pendingComponent with pendingMs: 0', async () => {
+ it('renders notFound when child beforeLoad throws and parent has an immediate pending component', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute({
@@ -2472,23 +2475,26 @@ describe('notFound in beforeLoad with pendingComponent', () => {
render( )
- // Wait for initial load
- await act(() => router.latestLoadPromise)
- expect(router.state.status).toBe('idle')
- expect(screen.getByTestId('home-page')).toBeInTheDocument()
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
- // Navigate to the child route that throws notFound in beforeLoad
await act(() => router.navigate({ to: '/parent/child' }))
- // The router status should eventually become idle
- await waitFor(() => {
- expect(router.state.status).toBe('idle')
- })
-
- expect(router.state.statusCode).toBe(404)
+ expect(await screen.findByTestId('parent-not-found')).toHaveTextContent(
+ 'Parent Not Found',
+ )
+ expect(screen.queryByTestId('root-not-found')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('pending-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('child-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('home-page')).not.toBeInTheDocument()
+ expect(router.state.matches.map((match) => match.routeId)).toEqual([
+ rootRoute.id,
+ parentRoute.id,
+ childRoute.id,
+ ])
+ expect(router.state.status).toBe('idle')
})
- it('should transition router.state.status to idle when child beforeLoad throws notFound and parent has NO pendingComponent', async () => {
+ it('renders notFound when child beforeLoad throws without a pending component', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute({
@@ -2521,19 +2527,20 @@ describe('notFound in beforeLoad with pendingComponent', () => {
render( )
- await act(() => router.latestLoadPromise)
- expect(router.state.status).toBe('idle')
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
await act(() => router.navigate({ to: '/child' }))
- await waitFor(() => {
- expect(router.state.status).toBe('idle')
- })
-
- expect(router.state.statusCode).toBe(404)
+ expect(await screen.findByTestId('child-not-found')).toHaveTextContent(
+ 'Child Not Found',
+ )
+ expect(screen.queryByTestId('root-not-found')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('child-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('home-page')).not.toBeInTheDocument()
+ expect(router.state.status).toBe('idle')
})
- it('should transition router.state.status to idle when nested child beforeLoad throws notFound WITHOUT pendingComponent', async () => {
+ it('renders notFound when nested child beforeLoad throws without a pending component', async () => {
const history = createMemoryHistory({ initialEntries: ['/'] })
const rootRoute = createRootRoute({
@@ -2580,89 +2587,145 @@ describe('notFound in beforeLoad with pendingComponent', () => {
render( )
- await act(() => router.latestLoadPromise)
- expect(router.state.status).toBe('idle')
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
await act(() => router.navigate({ to: '/parent/child' }))
- await waitFor(() => {
- expect(router.state.status).toBe('idle')
- })
-
- expect(router.state.statusCode).toBe(404)
+ expect(await screen.findByTestId('parent-not-found')).toHaveTextContent(
+ 'Parent Not Found',
+ )
+ expect(screen.queryByTestId('root-not-found')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('parent-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('child-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('home-page')).not.toBeInTheDocument()
+ expect(router.state.matches.map((match) => match.routeId)).toEqual([
+ rootRoute.id,
+ parentRoute.id,
+ childRoute.id,
+ ])
+ expect(router.state.status).toBe('idle')
})
- it('should transition router.state.status to idle when child async beforeLoad throws notFound and parent has pendingComponent with pendingMs: 0', async () => {
- const history = createMemoryHistory({ initialEntries: ['/'] })
+ it.each(['ancestor', 'child'] as const)(
+ 'renders the parent notFound after showing %s pending UI',
+ async (pendingOwner) => {
+ const history = createMemoryHistory({ initialEntries: ['/'] })
+ const beforeLoad = createControlledPromise()
- const rootRoute = createRootRoute({
- component: () => ,
- notFoundComponent: () => (
- Root Not Found
- ),
- })
+ const rootRoute = createRootRoute({
+ component: () => ,
+ notFoundComponent: () => (
+ Root Not Found
+ ),
+ })
- const indexRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: '/',
- component: () => (
-
- Go to child
-
- ),
- })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
- const parentRoute = createRoute({
- getParentRoute: () => rootRoute,
- path: '/parent',
- pendingMs: 0,
- pendingComponent: () => (
- Loading...
- ),
- component: () => (
-
- Parent
-
-
- ),
- notFoundComponent: () => (
- Parent Not Found
- ),
- })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ ...(pendingOwner === 'ancestor'
+ ? {
+ beforeLoad: () => beforeLoad,
+ pendingMs: 0,
+ pendingComponent: () => (
+ Loading ancestor...
+ ),
+ }
+ : {}),
+ component: () => (
+
+ Parent
+
+
+ ),
+ notFoundComponent: () => (
+ Parent Not Found
+ ),
+ })
- const childRoute = createRoute({
- getParentRoute: () => parentRoute,
- path: '/child',
- beforeLoad: async () => {
- await new Promise((resolve) => setTimeout(resolve, 10))
- throw notFound()
- },
- component: () => Child
,
- })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ ...(pendingOwner === 'child'
+ ? {
+ pendingMs: 0,
+ pendingComponent: () => (
+ Loading child...
+ ),
+ }
+ : {}),
+ beforeLoad: async () => {
+ if (pendingOwner === 'child') {
+ await beforeLoad
+ }
+ throw notFound()
+ },
+ component: () => Child
,
+ })
- const routeTree = rootRoute.addChildren([
- indexRoute,
- parentRoute.addChildren([childRoute]),
- ])
- const router = createRouter({ routeTree, history })
+ const routeTree = rootRoute.addChildren([
+ indexRoute,
+ parentRoute.addChildren([childRoute]),
+ ])
+ const router = createRouter({
+ routeTree,
+ history,
+ defaultPendingMinMs: 0,
+ })
- render( )
+ render( )
- // Wait for initial load
- await act(() => router.latestLoadPromise)
- expect(router.state.status).toBe('idle')
- expect(screen.getByTestId('home-page')).toBeInTheDocument()
+ expect(await screen.findByTestId('home-page')).toBeInTheDocument()
- // Navigate to the child route that throws notFound in beforeLoad
- await act(() => router.navigate({ to: '/parent/child' }))
+ vi.useFakeTimers()
+ let navigation!: Promise
+ const pendingTestId = `${pendingOwner}-pending`
+ try {
+ await act(async () => {
+ navigation = router.navigate({ to: '/parent/child' })
+ await vi.advanceTimersByTimeAsync(0)
+ })
- // The router status should eventually become idle
- await waitFor(() => {
- expect(router.state.status).toBe('idle')
- })
+ expect(screen.getByTestId(pendingTestId)).toBeInTheDocument()
+ if (pendingOwner === 'ancestor') {
+ expect(
+ screen.queryByTestId('parent-component'),
+ ).not.toBeInTheDocument()
+ } else {
+ expect(screen.getByTestId('parent-component')).toBeInTheDocument()
+ }
+ } finally {
+ try {
+ await act(async () => {
+ beforeLoad.resolve()
+ await navigation
+ })
+ } finally {
+ vi.useRealTimers()
+ }
+ }
- expect(router.state.statusCode).toBe(404)
- })
+ expect(screen.getByTestId('parent-not-found')).toHaveTextContent(
+ 'Parent Not Found',
+ )
+ expect(screen.queryByTestId('root-not-found')).not.toBeInTheDocument()
+ expect(screen.queryByTestId(pendingTestId)).not.toBeInTheDocument()
+ expect(screen.queryByTestId('parent-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('child-component')).not.toBeInTheDocument()
+ expect(screen.queryByTestId('home-page')).not.toBeInTheDocument()
+ expect(router.state.matches.map((match) => match.routeId)).toEqual([
+ rootRoute.id,
+ parentRoute.id,
+ childRoute.id,
+ ])
+ expect(router.state.status).toBe('idle')
+ },
+ )
})
describe('Router rewrite functionality', () => {
@@ -2867,7 +2930,6 @@ describe('Router rewrite functionality', () => {
},
})
render( )
- await router.latestLoadPromise
await waitFor(() => {
expect(screen.getByTestId('component')).toHaveTextContent('test Users')
})
@@ -3223,8 +3285,6 @@ describe('Router rewrite functionality', () => {
const navigateBtn = await screen.findByTestId('navigate-btn')
fireEvent.click(navigateBtn)
- await router.latestLoadPromise
-
await screen.findByTestId('dashboard')
// Router internal state should show the internal path
@@ -3612,7 +3672,6 @@ describe('basepath', () => {
})
expect(router.state.location.pathname).toBe('/')
- expect(router.state.statusCode).toBe(200)
},
)
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 0c4c1b4146..8a10e06339 100644
--- a/packages/react-router/tests/store-updates-during-navigation.test.tsx
+++ b/packages/react-router/tests/store-updates-during-navigation.test.tsx
@@ -136,7 +136,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(8)
+ expect(updates).toBe(7)
})
test('redirection in preload', async () => {
@@ -154,7 +154,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(1)
+ expect(updates).toBe(2)
})
test('sync beforeLoad', async () => {
@@ -196,7 +196,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(4)
+ expect(updates).toBe(3)
})
test('hover preload, then navigate, w/ async loaders', async () => {
diff --git a/packages/react-router/tests/transactional-loading.test.tsx b/packages/react-router/tests/transactional-loading.test.tsx
new file mode 100644
index 0000000000..a6769c6063
--- /dev/null
+++ b/packages/react-router/tests/transactional-loading.test.tsx
@@ -0,0 +1,328 @@
+import {
+ act,
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
+import {
+ Link,
+ Outlet,
+ RouterProvider,
+ createBrowserHistory,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+import type { RouterHistory } from '../src'
+
+type Deferred = {
+ promise: Promise
+ resolve: (value: T) => void
+}
+
+function deferred(): Deferred {
+ let resolve!: (value: T) => void
+ const promise = new Promise((resolver) => {
+ resolve = resolver
+ })
+ return { promise, resolve }
+}
+
+let history: RouterHistory
+
+beforeEach(() => {
+ history = createBrowserHistory()
+})
+
+afterEach(() => {
+ history.destroy()
+ window.history.replaceState(null, 'root', '/')
+ cleanup()
+ vi.useRealTimers()
+})
+
+describe('transactional route loading', () => {
+ test('publishes a parent and child background refresh atomically after the child observes fresh parent data', async () => {
+ const parentRefresh = deferred()
+ const childRefresh = deferred()
+ const childObservedFreshParent = deferred()
+ let parentLoads = 0
+ let childLoads = 0
+
+ const rootRoute = createRootRoute({
+ component: () => (
+ <>
+ Other route
+ Data route
+
+ >
+ ),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home route
,
+ })
+ const otherRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/other',
+ component: () => Other route content
,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ staleTime: 0,
+ gcTime: 60_000,
+ loader: async () => {
+ parentLoads += 1
+ if (parentLoads === 1) {
+ return 'parent-v1'
+ }
+ return parentRefresh.promise
+ },
+ component: () => (
+ <>
+ {parentRoute.useLoaderData()}
+
+ >
+ ),
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ staleTime: 0,
+ gcTime: 60_000,
+ loader: async ({ parentMatchPromise }) => {
+ childLoads += 1
+ const parentMatch = await parentMatchPromise
+ const parentData = parentMatch.loaderData as string
+ if (childLoads > 1) {
+ childObservedFreshParent.resolve(undefined)
+ await childRefresh.promise
+ }
+ return `child-saw-${parentData}`
+ },
+ component: () => {childRoute.useLoaderData()}
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ otherRoute,
+ parentRoute.addChildren([childRoute]),
+ ]),
+ history,
+ })
+
+ render( )
+
+ expect(await screen.findByText('Home route')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/')
+ })
+ fireEvent.click(screen.getByText('Data route'))
+
+ expect(await screen.findByText('parent-v1')).toBeInTheDocument()
+ expect(await screen.findByText('child-saw-parent-v1')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/parent/child')
+ })
+
+ fireEvent.click(screen.getByText('Other route'))
+ expect(await screen.findByText('Other route content')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/other')
+ })
+
+ fireEvent.click(screen.getByText('Data route'))
+
+ expect(await screen.findByText('parent-v1')).toBeInTheDocument()
+ expect(await screen.findByText('child-saw-parent-v1')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/parent/child')
+ })
+
+ await act(async () => {
+ parentRefresh.resolve('parent-v2')
+ await childObservedFreshParent.promise
+ })
+
+ expect(screen.getByText('parent-v1')).toBeInTheDocument()
+ expect(screen.getByText('child-saw-parent-v1')).toBeInTheDocument()
+ expect(screen.queryByText('parent-v2')).not.toBeInTheDocument()
+ expect(screen.queryByText('child-saw-parent-v2')).not.toBeInTheDocument()
+
+ await act(async () => {
+ childRefresh.resolve(undefined)
+ await Promise.resolve()
+ })
+
+ await waitFor(() => {
+ expect(screen.getByText('parent-v2')).toBeInTheDocument()
+ expect(screen.getByText('child-saw-parent-v2')).toBeInTheDocument()
+ expect(screen.queryByText('parent-v1')).not.toBeInTheDocument()
+ expect(screen.queryByText('child-saw-parent-v1')).not.toBeInTheDocument()
+ })
+ })
+
+ test('renders a leaf error at the leaf boundary when its background refresh fails', async () => {
+ let loads = 0
+ const rootRoute = createRootRoute({
+ component: () => (
+ <>
+ Root shell
+
+ >
+ ),
+ })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Open child,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ component: () => (
+ <>
+ Parent shell
+
+ >
+ ),
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ loader: {
+ staleReloadMode: 'background',
+ handler: () => {
+ loads++
+ if (loads > 1) {
+ throw new Error('background refresh failed')
+ }
+ return 'child data'
+ },
+ },
+ component: () => {childRoute.useLoaderData()}
,
+ errorComponent: () => Child refresh failed
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ indexRoute,
+ parentRoute.addChildren([childRoute]),
+ ]),
+ history,
+ })
+
+ render( )
+ fireEvent.click(await screen.findByText('Open child'))
+ expect(await screen.findByText('child data')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/parent/child')
+ })
+
+ await act(() => router.invalidate())
+
+ expect(await screen.findByText('Child refresh failed')).toBeInTheDocument()
+ expect(screen.getByText('Root shell')).toBeInTheDocument()
+ expect(screen.getByText('Parent shell')).toBeInTheDocument()
+ })
+
+ test('renders the first settled background failure after foreground work completes', async () => {
+ const rootRefresh = deferred()
+ const parentStarted = deferred()
+ const childStarted = deferred()
+ const parentGate = deferred()
+ const childGate = deferred()
+ const parentSettled = deferred()
+ const childSettled = deferred()
+ let rootLoads = 0
+ let parentLoads = 0
+ let childLoads = 0
+
+ const rootRoute = createRootRoute({
+ loader: {
+ staleReloadMode: 'blocking',
+ handler: async () => {
+ if (++rootLoads > 1) {
+ await rootRefresh.promise
+ }
+ },
+ },
+ component: Outlet,
+ })
+ const parentRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/parent',
+ loader: {
+ staleReloadMode: 'background',
+ handler: async () => {
+ if (++parentLoads > 1) {
+ parentStarted.resolve(undefined)
+ await parentGate.promise
+ parentSettled.resolve(undefined)
+ throw new Error('later parent failure')
+ }
+ return 'parent data'
+ },
+ },
+ component: Outlet,
+ errorComponent: () => Parent refresh failed
,
+ })
+ const childRoute = createRoute({
+ getParentRoute: () => parentRoute,
+ path: '/child',
+ loader: {
+ staleReloadMode: 'background',
+ handler: async () => {
+ if (++childLoads > 1) {
+ childStarted.resolve(undefined)
+ await childGate.promise
+ childSettled.resolve(undefined)
+ throw new Error('first child failure')
+ }
+ return 'child data'
+ },
+ },
+ component: () => {childRoute.useLoaderData()}
,
+ errorComponent: () => Child refresh failed
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([parentRoute.addChildren([childRoute])]),
+ history: createMemoryHistory({ initialEntries: ['/parent/child'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('child data')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ let invalidation!: Promise
+ await act(async () => {
+ invalidation = router.invalidate()
+ await Promise.all([parentStarted.promise, childStarted.promise])
+ })
+ await act(async () => {
+ childGate.resolve(undefined)
+ await childSettled.promise
+ parentGate.resolve(undefined)
+ await parentSettled.promise
+ })
+ expect(screen.getByText('child data')).toBeInTheDocument()
+
+ await act(async () => {
+ rootRefresh.resolve(undefined)
+ await invalidation
+ })
+
+ expect(screen.getByText('Child refresh failed')).toBeInTheDocument()
+ expect(screen.queryByText('Parent refresh failed')).not.toBeInTheDocument()
+ })
+})
diff --git a/packages/react-router/tests/transitioner-listener-errors.test.tsx b/packages/react-router/tests/transitioner-listener-errors.test.tsx
new file mode 100644
index 0000000000..9682b8f60c
--- /dev/null
+++ b/packages/react-router/tests/transitioner-listener-errors.test.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react'
+import { act, cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+const testCleanups: Array<() => void | Promise> = []
+
+afterEach(async () => {
+ while (testCleanups.length) {
+ await testCleanups.pop()!()
+ }
+ cleanup()
+})
+
+test('a throwing load-event listener cannot interrupt route hooks or later navigations', async () => {
+ const firstOnEnter = vi.fn()
+ const secondOnEnter = vi.fn()
+ const listenerError = new Error('onLoad listener failed')
+ const laterOnLoad = vi.fn()
+ const loadedPaths: Array = []
+
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index route
,
+ })
+ const firstRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/first',
+ onEnter: firstOnEnter,
+ component: () => First route
,
+ })
+ const secondRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/second',
+ onEnter: secondOnEnter,
+ component: () => Second route
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, firstRoute, secondRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Index route')).toBeInTheDocument()
+
+ const unsubscribe = router.subscribe('onLoad', (event) => {
+ if (event.toLocation.pathname === '/first') {
+ throw listenerError
+ }
+ })
+ const unsubscribeLater = router.subscribe('onLoad', (event) => {
+ if (event.toLocation.pathname !== '/') {
+ loadedPaths.push(event.toLocation.pathname)
+ laterOnLoad(event)
+ }
+ })
+ testCleanups.push(unsubscribe, unsubscribeLater)
+
+ await act(() => router.navigate({ to: '/first' }))
+
+ expect(screen.getByText('First route')).toBeInTheDocument()
+ expect(loadedPaths).toEqual(['/first'])
+
+ unsubscribe()
+ await act(() => router.navigate({ to: '/second' }))
+
+ expect(screen.getByText('Second route')).toBeInTheDocument()
+ expect(firstOnEnter).toHaveBeenCalledTimes(1)
+ expect(secondOnEnter).toHaveBeenCalledTimes(1)
+ expect(laterOnLoad).toHaveBeenCalledTimes(2)
+ expect(loadedPaths).toEqual(['/first', '/second'])
+})
diff --git a/packages/react-router/tests/transitioner-remount.test.tsx b/packages/react-router/tests/transitioner-remount.test.tsx
new file mode 100644
index 0000000000..76cc6393ce
--- /dev/null
+++ b/packages/react-router/tests/transitioner-remount.test.tsx
@@ -0,0 +1,91 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, render, screen, waitFor } from '@testing-library/react'
+import {
+ Outlet,
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+})
+
+function setup() {
+ const rootRoute = createRootRoute({ component: () => })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const nextRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/next',
+ component: () => Next
,
+ })
+ const history = createMemoryHistory({ initialEntries: ['/'] })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, nextRoute]),
+ history,
+ })
+ return { history, router }
+}
+
+describe('Transitioner remount', () => {
+ // Zombie-router pin: once the provider unmounts, the history subscription
+ // must be torn down so a later history change never re-drives the router.
+ it('does not load after the provider unmounts', async () => {
+ const { history, router } = setup()
+ const loadSpy = vi.spyOn(router, 'load')
+
+ const first = render( )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ first.unmount()
+ loadSpy.mockClear()
+
+ // A history change while unmounted must not reach the router.
+ history.push('/next')
+ await new Promise((resolve) => setTimeout(resolve, 0))
+
+ expect(loadSpy).not.toHaveBeenCalled()
+ // Raw history advanced...
+ expect(router.history.location.pathname).toBe('/next')
+ // ...but the router never processed it, so its committed state stayed put.
+ expect(router.state.location.pathname).toBe('/')
+
+ loadSpy.mockRestore()
+ })
+
+ // Remounting the same router instance must re-establish the subscription so
+ // navigation keeps working, without leaving a stale duplicate behind.
+ it('re-subscribes and loads on remount with the same router', async () => {
+ const { history, router } = setup()
+ // Spy before the first mount so the subscription captures the spy by
+ // reference - both the first and second mounts subscribe with it.
+ const loadSpy = vi.spyOn(router, 'load')
+
+ const first = render( )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ first.unmount()
+
+ render( )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ // A push on the remounted provider must drive exactly one load, proving
+ // the subscription was re-established (and is singular).
+ loadSpy.mockClear()
+ history.push('/next')
+ expect(await screen.findByText('Next')).toBeInTheDocument()
+ expect(router.state.location.pathname).toBe('/next')
+ await waitFor(() => expect(loadSpy).toHaveBeenCalledTimes(1))
+
+ loadSpy.mockRestore()
+ })
+})
diff --git a/packages/react-router/tests/transitioner-render-ack.test.tsx b/packages/react-router/tests/transitioner-render-ack.test.tsx
new file mode 100644
index 0000000000..74bbe7d485
--- /dev/null
+++ b/packages/react-router/tests/transitioner-render-ack.test.tsx
@@ -0,0 +1,258 @@
+import { StrictMode, act } from 'react'
+import { cleanup, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ Outlet,
+ RouterProvider,
+ createControlledPromise,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+} from '../src'
+
+const testCleanups: Array<() => void> = []
+
+afterEach(() => {
+ while (testCleanups.length) {
+ testCleanups.pop()!()
+ }
+ cleanup()
+ vi.useRealTimers()
+ vi.unstubAllEnvs()
+})
+
+test('a route lifecycle callback cannot strand a production navigation', async () => {
+ vi.stubEnv('NODE_ENV', 'production')
+ expect(process.env.NODE_ENV).toBe('production')
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const error = new Error('onEnter failed')
+ const onEnter = vi.fn(() => {
+ throw error
+ })
+ const nextRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/next',
+ onEnter,
+ component: () => Next
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, nextRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ const globalReport = vi.fn()
+ const preventGlobalReport = (event: ErrorEvent) => {
+ if (event.error === error) {
+ globalReport()
+ event.preventDefault()
+ }
+ }
+ window.addEventListener('error', preventGlobalReport)
+ testCleanups.push(() => {
+ window.removeEventListener('error', preventGlobalReport)
+ })
+
+ let settled = false
+ const navigation = router.navigate({ to: '/next' }).then(
+ () => {
+ settled = true
+ },
+ () => {
+ settled = true
+ },
+ )
+ await waitFor(() => expect(settled).toBe(true))
+ await navigation
+
+ expect(onEnter).toHaveBeenCalledOnce()
+ expect(globalReport).not.toHaveBeenCalled()
+ expect(screen.getByText('Next')).toBeInTheDocument()
+ expect(router.state.status).toBe('idle')
+
+ await router.navigate({ to: '/' })
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+})
+
+test('same-location invalidation resolves after its refreshed DOM commits', async () => {
+ let generation = 0
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: {
+ staleReloadMode: 'blocking',
+ handler: () => ++generation,
+ },
+ component: () => Generation {indexRoute.useLoaderData()}
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Generation 1')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/')
+ })
+
+ const refreshedDomWasVisible: Array = []
+ const unsubscribe = router.subscribe('onResolved', () => {
+ refreshedDomWasVisible.push(screen.queryByText('Generation 2') !== null)
+ })
+ testCleanups.push(unsubscribe)
+
+ await act(() => router.invalidate())
+ expect(screen.getByText('Generation 2')).toBeInTheDocument()
+ expect(screen.queryByText('Generation 1')).not.toBeInTheDocument()
+ expect(refreshedDomWasVisible).toEqual([true])
+})
+
+test('an immediately completed background refresh cannot replace the acknowledged foreground generation', async () => {
+ let generation = 0
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ loader: {
+ staleReloadMode: 'background',
+ handler: () => ++generation,
+ },
+ component: () => Generation {indexRoute.useLoaderData()}
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Generation 1')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+
+ await act(() => router.invalidate())
+
+ expect(await screen.findByText('Generation 2')).toBeInTheDocument()
+ expect(router.state.status).toBe('idle')
+})
+
+test('a navigation started by route lifecycle keeps the pending minimum of its own render', async () => {
+ const slowLoader = createControlledPromise()
+ let nestedNavigation: Promise | undefined
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const redirectorRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/redirector',
+ onEnter: () => {
+ nestedNavigation = router.navigate({ to: '/slow' })
+ },
+ component: () => Redirector
,
+ })
+ const slowRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/slow',
+ pendingMs: 0,
+ pendingMinMs: 100,
+ pendingComponent: () => Slow pending
,
+ loader: () => slowLoader,
+ component: () => Slow done
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, redirectorRoute, slowRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+ await waitFor(() => expect(router.state.status).toBe('idle'))
+ vi.useFakeTimers()
+
+ let firstNavigation!: Promise
+ await act(async () => {
+ firstNavigation = router.navigate({ to: '/redirector' })
+ await vi.advanceTimersByTimeAsync(0)
+ })
+
+ expect(nestedNavigation).toBeDefined()
+ expect(screen.getByText('Slow pending')).toBeInTheDocument()
+
+ await act(async () => {
+ slowLoader.resolve()
+ await Promise.resolve()
+ })
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(99)
+ })
+ expect(screen.getByText('Slow pending')).toBeInTheDocument()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1)
+ await Promise.all([firstNavigation, nestedNavigation!])
+ })
+ expect(screen.queryByText('Slow pending')).not.toBeInTheDocument()
+ expect(screen.getByText('Slow done')).toBeInTheDocument()
+})
+
+test('StrictMode effect replay preserves renderer commit sequencing', async () => {
+ const rootRoute = createRootRoute({ component: Outlet })
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Index
,
+ })
+ const nextRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/next',
+ component: () => Next
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, nextRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ render(
+
+
+ ,
+ )
+ expect(await screen.findByText('Index')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(router.state.status).toBe('idle')
+ expect(router.state.resolvedLocation?.pathname).toBe('/')
+ })
+
+ const eventLog: Array = []
+ const unsubscribers = [
+ router.subscribe('onResolved', (event) => {
+ if (event.toLocation.pathname === '/next') {
+ eventLog.push('onResolved:/next')
+ }
+ }),
+ router.subscribe('onRendered', (event) => {
+ if (event.toLocation.pathname === '/next') {
+ eventLog.push('onRendered:/next')
+ }
+ }),
+ ]
+ testCleanups.push(...unsubscribers)
+
+ await act(() => router.navigate({ to: '/next' }))
+ expect(eventLog).toEqual(['onResolved:/next', 'onRendered:/next'])
+ expect(screen.getByText('Next')).toBeInTheDocument()
+ expect(screen.queryByText('Index')).not.toBeInTheDocument()
+})
diff --git a/packages/react-router/tests/useMatch.test.tsx b/packages/react-router/tests/useMatch.test.tsx
index 98438f8cd0..ac4c52f6cc 100644
--- a/packages/react-router/tests/useMatch.test.tsx
+++ b/packages/react-router/tests/useMatch.test.tsx
@@ -1,5 +1,11 @@
import { afterEach, describe, expect, test, vi } from 'vitest'
-import { cleanup, render, screen, waitFor } from '@testing-library/react'
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react'
import {
Link,
Outlet,
@@ -94,6 +100,151 @@ describe('useMatch', () => {
})
})
+ test('tracks presentation generations across replacement and re-entry', async () => {
+ function RootComponent() {
+ const targetedRevision = useMatch({
+ from: '/item',
+ shouldThrow: false,
+ select: (match) => match.loaderData,
+ })
+
+ return (
+ <>
+
+ {targetedRevision === undefined
+ ? 'Targeted absent'
+ : `Targeted revision ${targetedRevision}`}
+
+
+ Revision 2
+
+
+ Revision 3
+
+ Other
+
+ >
+ )
+ }
+
+ function ItemComponent() {
+ const nearestRevision = useMatch({
+ strict: false,
+ shouldThrow: false,
+ select: (match) => match.loaderData as number,
+ })
+ return Nearest revision {nearestRevision}
+ }
+
+ const rootRoute = createRootRoute({ component: RootComponent })
+ const itemRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/item',
+ validateSearch: (search: Record) => ({
+ revision: Number(search.revision),
+ }),
+ loaderDeps: ({ search }) => ({ revision: search.revision }),
+ loader: ({ deps }) => deps.revision,
+ component: ItemComponent,
+ })
+ const otherRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/other',
+ component: () => Other route
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([itemRoute, otherRoute]),
+ history: createMemoryHistory({ initialEntries: ['/item?revision=1'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Nearest revision 1')).toBeInTheDocument()
+ expect(screen.getByTestId('targeted-match')).toHaveTextContent(
+ 'Targeted revision 1',
+ )
+
+ fireEvent.click(screen.getByText('Revision 2'))
+ expect(await screen.findByText('Nearest revision 2')).toBeInTheDocument()
+ expect(screen.getByTestId('targeted-match')).toHaveTextContent(
+ 'Targeted revision 2',
+ )
+
+ fireEvent.click(screen.getByText('Other'))
+ expect(await screen.findByText('Other route')).toBeInTheDocument()
+ expect(screen.getByTestId('targeted-match')).toHaveTextContent(
+ 'Targeted absent',
+ )
+
+ fireEvent.click(screen.getByText('Revision 3'))
+ expect(await screen.findByText('Nearest revision 3')).toBeInTheDocument()
+ expect(screen.getByTestId('targeted-match')).toHaveTextContent(
+ 'Targeted revision 3',
+ )
+ })
+
+ test('renders a route generation that re-enters before an intermediate route renders', async () => {
+ function RootComponent() {
+ return (
+ <>
+ Other
+
+ >
+ )
+ }
+
+ function ItemComponent() {
+ const revision = useMatch({
+ strict: false,
+ select: (match) => match.loaderData as number,
+ })
+ return Item revision {revision}
+ }
+
+ const rootRoute = createRootRoute({ component: RootComponent })
+ const itemRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/item',
+ validateSearch: (search: Record) => ({
+ revision: Number(search.revision),
+ }),
+ loaderDeps: ({ search }) => ({ revision: search.revision }),
+ loader: ({ deps }) => deps.revision,
+ component: ItemComponent,
+ })
+ const otherRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/other',
+ component: () => Other route
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([itemRoute, otherRoute]),
+ history: createMemoryHistory({ initialEntries: ['/item?revision=1'] }),
+ })
+
+ render( )
+ expect(await screen.findByText('Item revision 1')).toBeInTheDocument()
+
+ let returnNavigation: Promise | undefined
+ const unsubscribe = router.subscribe('onLoad', (event) => {
+ if (event.toLocation.pathname === '/other') {
+ returnNavigation = router.navigate({
+ to: '/item',
+ search: { revision: 2 },
+ })
+ }
+ })
+ try {
+ fireEvent.click(screen.getByText('Other'))
+ await waitFor(() => expect(returnNavigation).toBeDefined())
+ await returnNavigation
+
+ expect(await screen.findByText('Item revision 2')).toBeInTheDocument()
+ expect(screen.queryByText('Other route')).not.toBeInTheDocument()
+ } finally {
+ unsubscribe()
+ }
+ })
+
describe('when match is not found', () => {
test.each([undefined, true])(
'throws if shouldThrow = %s',
diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx
index a473e2c8b2..1ec1072a26 100644
--- a/packages/react-router/tests/useNavigate.test.tsx
+++ b/packages/react-router/tests/useNavigate.test.tsx
@@ -7,6 +7,7 @@ import {
fireEvent,
render,
screen,
+ waitFor,
} from '@testing-library/react'
import { z } from 'zod'
@@ -2312,9 +2313,11 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
path: 'param/$param',
component: function ParamRoute() {
const navigate = useNavigate()
+ const params = paramRoute.useParams()
return (
<>
Param Route
+ {params.param}
navigate({ from: paramRoute.fullPath, to: './a' })
@@ -2415,9 +2418,9 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
// Click the link and ensure the new location
fireEvent.click(parentLink)
- await router.latestLoadPromise
-
- expect(window.location.pathname).toBe(`${basepath}/a`)
+ await waitFor(() => {
+ expect(window.location.pathname).toBe(`${basepath}/a`)
+ })
})
test('should navigate to the parent route and keep params', async () => {
@@ -2435,9 +2438,9 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
// Click the link and ensure the new location
fireEvent.click(parentLink)
- await router.latestLoadPromise
-
- expect(window.location.pathname).toBe(`${basepath}/param/foo/a`)
+ await waitFor(() => {
+ expect(window.location.pathname).toBe(`${basepath}/param/foo/a`)
+ })
})
test('should navigate to the parent route and change params', async () => {
@@ -2457,9 +2460,9 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
// Click the link and ensure the new location
fireEvent.click(parentLink)
- await router.latestLoadPromise
-
- expect(window.location.pathname).toBe(`${basepath}/param/bar/a`)
+ await waitFor(() => {
+ expect(window.location.pathname).toBe(`${basepath}/param/bar/a`)
+ })
})
test('should navigate to a relative link based on render location with basepath', async () => {
@@ -2476,9 +2479,9 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
// Click the link and ensure the new location
fireEvent.click(relativeLink)
- await router.latestLoadPromise
-
- expect(window.location.pathname).toBe(`${basepath}/param/foo/a`)
+ await waitFor(() => {
+ expect(window.location.pathname).toBe(`${basepath}/param/foo/a`)
+ })
})
test('should navigate to a parent link based on render location', async () => {
@@ -2497,9 +2500,9 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
// Click the link and ensure the new location
fireEvent.click(relativeLink)
- await router.latestLoadPromise
-
- expect(window.location.pathname).toBe(`${basepath}/param/foo`)
+ await waitFor(() => {
+ expect(window.location.pathname).toBe(`${basepath}/param/foo`)
+ })
})
test('should navigate to a parent link based on active location', async () => {
@@ -2515,9 +2518,9 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
// Click the link and ensure the new location
fireEvent.click(relativeLink)
- await router.latestLoadPromise
-
- expect(window.location.pathname).toBe(`${basepath}/param/foo/a`)
+ await waitFor(() => {
+ expect(window.location.pathname).toBe(`${basepath}/param/foo/a`)
+ })
})
test('should navigate to same route with different params', async () => {
@@ -2527,15 +2530,18 @@ describe.each([{ basepath: '' }, { basepath: '/basepath' }])(
await act(async () => {
history.push(`${basepath}/param/foo/a/b`)
- await router.latestLoadPromise
})
- expect(window.location.pathname).toBe(`${basepath}/param/foo/a/b`)
+ await waitFor(() => {
+ expect(window.location.pathname).toBe(`${basepath}/param/foo/a/b`)
+ })
+ expect(await screen.findByTestId('param-value')).toHaveTextContent('foo')
- const btn = screen.getByTestId('btn-param-bar')
+ const btn = await screen.findByTestId('btn-param-bar')
fireEvent.click(btn)
- await router.latestLoadPromise
-
- expect(window.location.pathname).toBe(`${basepath}/param/bar/a/b`)
+ await waitFor(() => {
+ expect(screen.getByTestId('param-value')).toHaveTextContent('bar')
+ expect(window.location.pathname).toBe(`${basepath}/param/bar/a/b`)
+ })
})
},
)
diff --git a/packages/react-start-client/CHANGELOG.md b/packages/react-start-client/CHANGELOG.md
index a3dd36024e..261be673df 100644
--- a/packages/react-start-client/CHANGELOG.md
+++ b/packages/react-start-client/CHANGELOG.md
@@ -1,5 +1,23 @@
# @tanstack/react-start-client
+## 1.168.17
+
+### Patch Changes
+
+- Updated dependencies [[`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd)]:
+ - @tanstack/router-core@1.171.16
+ - @tanstack/react-router@1.170.19
+ - @tanstack/start-client-core@1.170.15
+
+## 1.168.16
+
+### Patch Changes
+
+- Updated dependencies [[`e2dd204`](https://github.com/TanStack/router/commit/e2dd2049cb42eb219d3b447b8605066d19d9c1fa)]:
+ - @tanstack/router-core@1.171.15
+ - @tanstack/react-router@1.170.18
+ - @tanstack/start-client-core@1.170.14
+
## 1.168.15
### Patch Changes
diff --git a/packages/react-start-client/package.json b/packages/react-start-client/package.json
index 634c565af9..1b85f031c3 100644
--- a/packages/react-start-client/package.json
+++ b/packages/react-start-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start-client",
- "version": "1.168.15",
+ "version": "1.168.17",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -30,12 +30,12 @@
"test:unit:dev": "vitest --watch",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"build": "vite build"
},
diff --git a/packages/react-start-client/src/hydrateStart.ts b/packages/react-start-client/src/hydrateStart.ts
index 22f4848de7..21f1fa2039 100644
--- a/packages/react-start-client/src/hydrateStart.ts
+++ b/packages/react-start-client/src/hydrateStart.ts
@@ -4,9 +4,6 @@ import type { AnyRouter } from '@tanstack/router-core'
/**
* React-specific wrapper for hydrateStart that signals hydration completion
*/
-export async function hydrateStart(): Promise {
- const router = await coreHydrateStart()
- // Signal that router hydration is complete so cleanup can happen if stream has ended
- window.$_TSR?.h()
- return router
+export function hydrateStart(): Promise {
+ return coreHydrateStart().finally(() => window.$_TSR?.h())
}
diff --git a/packages/react-start-client/src/tests/hydrateStart.test.ts b/packages/react-start-client/src/tests/hydrateStart.test.ts
new file mode 100644
index 0000000000..2d34400009
--- /dev/null
+++ b/packages/react-start-client/src/tests/hydrateStart.test.ts
@@ -0,0 +1,33 @@
+import { afterEach, expect, test, vi } from 'vitest'
+import { hydrateStart } from '../hydrateStart'
+
+const coreHydrateStart = vi.hoisted(() => vi.fn())
+
+vi.mock('@tanstack/start-client-core/client', () => ({
+ hydrateStart: coreHydrateStart,
+}))
+
+afterEach(() => {
+ delete window.$_TSR
+ coreHydrateStart.mockReset()
+})
+
+test('signals streaming cleanup after hydration succeeds', async () => {
+ const router = {}
+ coreHydrateStart.mockResolvedValue(router)
+ const hydrated = vi.fn()
+ window.$_TSR = { h: hydrated } as any
+
+ await expect(hydrateStart()).resolves.toBe(router)
+ expect(hydrated).toHaveBeenCalledTimes(1)
+})
+
+test('signals streaming cleanup without hiding a hydration failure', async () => {
+ const error = new Error('hydration failed')
+ coreHydrateStart.mockRejectedValue(error)
+ const hydrated = vi.fn()
+ window.$_TSR = { h: hydrated } as any
+
+ await expect(hydrateStart()).rejects.toBe(error)
+ expect(hydrated).toHaveBeenCalledTimes(1)
+})
diff --git a/packages/react-start-rsc/CHANGELOG.md b/packages/react-start-rsc/CHANGELOG.md
index 69627dbebb..90e0807dd2 100644
--- a/packages/react-start-rsc/CHANGELOG.md
+++ b/packages/react-start-rsc/CHANGELOG.md
@@ -1,5 +1,78 @@
# @tanstack/react-start-rsc
+## 0.1.35
+
+### Patch Changes
+
+- Updated dependencies [[`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd)]:
+ - @tanstack/router-core@1.171.16
+ - @tanstack/react-router@1.170.19
+ - @tanstack/start-client-core@1.170.15
+ - @tanstack/start-plugin-core@1.171.27
+ - @tanstack/start-storage-context@1.167.18
+
+## 0.1.34
+
+### Patch Changes
+
+- [#7944](https://github.com/TanStack/router/pull/7944) [`65f7b7f`](https://github.com/TanStack/router/commit/65f7b7f791272f2ea581a1fe0fcd43183bc01162) - Read request cancellation from the Start storage context so RSC helpers do not pull the Start server barrel into the RSC module graph.
+
+- Updated dependencies [[`65f7b7f`](https://github.com/TanStack/router/commit/65f7b7f791272f2ea581a1fe0fcd43183bc01162)]:
+ - @tanstack/start-plugin-core@1.171.26
+
+## 0.1.33
+
+### Patch Changes
+
+- Updated dependencies [[`7592555`](https://github.com/TanStack/router/commit/7592555b86c968efbc8c817ac0cf6fdae60aabe0)]:
+ - @tanstack/start-plugin-core@1.171.25
+
+## 0.1.32
+
+### Patch Changes
+
+- [#7900](https://github.com/TanStack/router/pull/7900) [`fc83c03`](https://github.com/TanStack/router/commit/fc83c0383f956c3ca02e5e027666c917d7e8b07a) - Raise the `@vitejs/plugin-rsc` peer range to `>=0.5.30`. Versions in `0.5.20 - 0.5.29` suppress client HMR for a route component co-located with a `createServerFn`, fixed upstream in `@vitejs/plugin-rsc@0.5.30`.
+
+## 0.1.31
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/start-plugin-core@1.171.24
+
+## 0.1.30
+
+### Patch Changes
+
+- Updated dependencies [[`ffdd64e`](https://github.com/TanStack/router/commit/ffdd64e842acacbc9d368a4803a9e474e9f0c0ff)]:
+ - @tanstack/start-plugin-core@1.171.23
+
+## 0.1.29
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/start-plugin-core@1.171.22
+
+## 0.1.28
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/start-plugin-core@1.171.21
+
+## 0.1.27
+
+### Patch Changes
+
+- Updated dependencies [[`e2dd204`](https://github.com/TanStack/router/commit/e2dd2049cb42eb219d3b447b8605066d19d9c1fa)]:
+ - @tanstack/router-core@1.171.15
+ - @tanstack/react-router@1.170.18
+ - @tanstack/start-client-core@1.170.14
+ - @tanstack/start-plugin-core@1.171.20
+ - @tanstack/start-server-core@1.169.17
+ - @tanstack/start-storage-context@1.167.17
+
## 0.1.26
### Patch Changes
diff --git a/packages/react-start-rsc/eslint.config.js b/packages/react-start-rsc/eslint.config.js
index 931f0ec774..daf1baa88a 100644
--- a/packages/react-start-rsc/eslint.config.js
+++ b/packages/react-start-rsc/eslint.config.js
@@ -22,6 +22,33 @@ export default [
'react-hooks/rules-of-hooks': 'error',
},
},
+ {
+ name: 'react-start-rsc/import-boundaries',
+ files: ['src/**/*.{ts,tsx}'],
+ rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ paths: [
+ {
+ name: '@tanstack/start-server-core',
+ message:
+ 'Import from a dedicated @tanstack/start-server-core subpath to avoid pulling the full server barrel into the RSC module graph.',
+ },
+ ],
+ },
+ ],
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector:
+ "ImportExpression[source.value='@tanstack/start-server-core']",
+ message:
+ 'Dynamically import a dedicated @tanstack/start-server-core subpath instead of the root barrel.',
+ },
+ ],
+ },
+ },
{
files: ['**/__tests__/**'],
rules: {
diff --git a/packages/react-start-rsc/package.json b/packages/react-start-rsc/package.json
index 498a5aff80..7280c24c39 100644
--- a/packages/react-start-rsc/package.json
+++ b/packages/react-start-rsc/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start-rsc",
- "version": "0.1.26",
+ "version": "0.1.35",
"description": "React Server Components support for TanStack Start",
"author": "Tanner Linsley",
"license": "MIT",
@@ -29,12 +29,12 @@
"test:unit:dev": "vitest --watch",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"build": "vite build"
},
@@ -103,7 +103,6 @@
"@tanstack/start-client-core": "workspace:*",
"@tanstack/start-fn-stubs": "workspace:*",
"@tanstack/start-plugin-core": "workspace:*",
- "@tanstack/start-server-core": "workspace:*",
"@tanstack/start-storage-context": "workspace:*",
"pathe": "^2.0.3"
},
@@ -111,12 +110,12 @@
"@rspack/core": "2.0.0",
"@testing-library/react": "^16.2.0",
"@vitejs/plugin-react": "^4.3.4",
- "@vitejs/plugin-rsc": "^0.5.20",
+ "@vitejs/plugin-rsc": "^0.5.30",
"react-server-dom-rspack": "^0.0.2"
},
"peerDependencies": {
"@rspack/core": ">=2.0.0-0",
- "@vitejs/plugin-rsc": ">=0.5.20",
+ "@vitejs/plugin-rsc": ">=0.5.30",
"react": ">=18.0.0 || >=19.0.0",
"react-dom": ">=18.0.0 || >=19.0.0",
"react-server-dom-rspack": ">=0.0.2"
diff --git a/packages/react-start-rsc/src/createCompositeComponent.ts b/packages/react-start-rsc/src/createCompositeComponent.ts
index c6a43b0f9d..02a840dc64 100644
--- a/packages/react-start-rsc/src/createCompositeComponent.ts
+++ b/packages/react-start-rsc/src/createCompositeComponent.ts
@@ -1,6 +1,5 @@
import { createElement } from 'react'
import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
-import { getRequest } from '@tanstack/start-server-core'
import { getStartContext } from '@tanstack/start-storage-context'
import { sanitizeSlotArgs } from './slotUsageSanitizer'
import { ReplayableStream } from './ReplayableStream'
@@ -99,7 +98,7 @@ export async function createCompositeComponent(
// SSR path: buffer stream for replay, pre-decode for synchronous rendering
if (isRouterRequest && ssrHandler) {
- const signal = getRequest().signal
+ const signal = ctx.request.signal
const stream = new ReplayableStream(flightStream, { signal })
// Pre-decode during loader phase for synchronous SSR rendering
diff --git a/packages/react-start-rsc/src/renderServerComponent.ts b/packages/react-start-rsc/src/renderServerComponent.ts
index d282d63111..4ef6037821 100644
--- a/packages/react-start-rsc/src/renderServerComponent.ts
+++ b/packages/react-start-rsc/src/renderServerComponent.ts
@@ -1,5 +1,4 @@
import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
-import { getRequest } from '@tanstack/start-server-core'
import { getStartContext } from '@tanstack/start-storage-context'
import { ReplayableStream } from './ReplayableStream'
import { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from './ServerComponentTypes'
@@ -72,7 +71,7 @@ export async function renderServerComponent(
// SSR path: buffer stream for replay, pre-decode for synchronous rendering
if (isRouterRequest && ssrHandler) {
- const signal = getRequest().signal
+ const signal = ctx.request.signal
const stream = new ReplayableStream(flightStream, { signal })
// Pre-decode during loader phase for synchronous SSR rendering
diff --git a/packages/react-start-rsc/tests/createServerComponent.test-d.tsx b/packages/react-start-rsc/tests/createServerComponent.test-d.tsx
index 9a161e2345..320ef4f30a 100644
--- a/packages/react-start-rsc/tests/createServerComponent.test-d.tsx
+++ b/packages/react-start-rsc/tests/createServerComponent.test-d.tsx
@@ -1,21 +1,10 @@
-import { expectTypeOf, test, vi } from 'vitest'
+import { expectTypeOf, test } from 'vitest'
import type {
CompositeComponentResult,
ValidateCompositeComponent,
} from '../src/ServerComponentTypes'
import { CompositeComponent } from '../src/CompositeComponent'
-vi.mock('@tanstack/start-server-core', () => {
- return {
- getRequest: () => undefined,
- }
-})
-
-vi.mock('@tanstack/start-storage-context', () => {
- return {
- getStartContext: () => undefined,
- }
-})
import { JSX } from 'react'
test('when a server component is created with no props', () => {
diff --git a/packages/react-start-server/CHANGELOG.md b/packages/react-start-server/CHANGELOG.md
index a56f3cc481..d1ba326d96 100644
--- a/packages/react-start-server/CHANGELOG.md
+++ b/packages/react-start-server/CHANGELOG.md
@@ -1,5 +1,30 @@
# @tanstack/react-start-server
+## 1.167.24
+
+### Patch Changes
+
+- Updated dependencies [[`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd)]:
+ - @tanstack/router-core@1.171.16
+ - @tanstack/react-router@1.170.19
+ - @tanstack/start-server-core@1.169.19
+
+## 1.167.23
+
+### Patch Changes
+
+- Updated dependencies [[`65f7b7f`](https://github.com/TanStack/router/commit/65f7b7f791272f2ea581a1fe0fcd43183bc01162)]:
+ - @tanstack/start-server-core@1.169.18
+
+## 1.167.22
+
+### Patch Changes
+
+- Updated dependencies [[`e2dd204`](https://github.com/TanStack/router/commit/e2dd2049cb42eb219d3b447b8605066d19d9c1fa)]:
+ - @tanstack/router-core@1.171.15
+ - @tanstack/react-router@1.170.18
+ - @tanstack/start-server-core@1.169.17
+
## 1.167.21
### Patch Changes
diff --git a/packages/react-start-server/package.json b/packages/react-start-server/package.json
index 62c02c7268..3e5b0f67db 100644
--- a/packages/react-start-server/package.json
+++ b/packages/react-start-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start-server",
- "version": "1.167.21",
+ "version": "1.167.24",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -29,12 +29,12 @@
"test:unit": "exit 0; vitest",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"build": "vite build"
},
@@ -66,7 +66,8 @@
"@vitejs/plugin-react": "^4.3.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
- "typescript": "^6.0.2",
+ "@typescript/native": "npm:typescript@^7.0.2",
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "*"
},
"peerDependencies": {
diff --git a/packages/react-start/CHANGELOG.md b/packages/react-start/CHANGELOG.md
index 90a0ad039f..d579571791 100644
--- a/packages/react-start/CHANGELOG.md
+++ b/packages/react-start/CHANGELOG.md
@@ -1,5 +1,88 @@
# @tanstack/react-start
+## 1.168.36
+
+### Patch Changes
+
+- Updated dependencies [[`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd)]:
+ - @tanstack/react-router@1.170.19
+ - @tanstack/react-start-client@1.168.17
+ - @tanstack/react-start-rsc@0.1.35
+ - @tanstack/react-start-server@1.167.24
+ - @tanstack/start-client-core@1.170.15
+ - @tanstack/start-plugin-core@1.171.27
+ - @tanstack/start-server-core@1.169.19
+
+## 1.168.35
+
+### Patch Changes
+
+- Updated dependencies [[`65f7b7f`](https://github.com/TanStack/router/commit/65f7b7f791272f2ea581a1fe0fcd43183bc01162), [`65f7b7f`](https://github.com/TanStack/router/commit/65f7b7f791272f2ea581a1fe0fcd43183bc01162)]:
+ - @tanstack/start-server-core@1.169.18
+ - @tanstack/start-plugin-core@1.171.26
+ - @tanstack/react-start-rsc@0.1.34
+ - @tanstack/react-start-server@1.167.23
+
+## 1.168.34
+
+### Patch Changes
+
+- Updated dependencies [[`7592555`](https://github.com/TanStack/router/commit/7592555b86c968efbc8c817ac0cf6fdae60aabe0)]:
+ - @tanstack/start-plugin-core@1.171.25
+ - @tanstack/react-start-rsc@0.1.33
+
+## 1.168.33
+
+### Patch Changes
+
+- Updated dependencies [[`fc83c03`](https://github.com/TanStack/router/commit/fc83c0383f956c3ca02e5e027666c917d7e8b07a)]:
+ - @tanstack/react-start-rsc@0.1.32
+
+## 1.168.32
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/start-plugin-core@1.171.24
+ - @tanstack/react-start-rsc@0.1.31
+
+## 1.168.31
+
+### Patch Changes
+
+- Updated dependencies [[`ffdd64e`](https://github.com/TanStack/router/commit/ffdd64e842acacbc9d368a4803a9e474e9f0c0ff)]:
+ - @tanstack/start-plugin-core@1.171.23
+ - @tanstack/react-start-rsc@0.1.30
+
+## 1.168.30
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/start-plugin-core@1.171.22
+ - @tanstack/react-start-rsc@0.1.29
+
+## 1.168.29
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/start-plugin-core@1.171.21
+ - @tanstack/react-start-rsc@0.1.28
+
+## 1.168.28
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/react-router@1.170.18
+ - @tanstack/react-start-client@1.168.16
+ - @tanstack/react-start-rsc@0.1.27
+ - @tanstack/react-start-server@1.167.22
+ - @tanstack/start-client-core@1.170.14
+ - @tanstack/start-plugin-core@1.171.20
+ - @tanstack/start-server-core@1.169.17
+
## 1.168.27
### Patch Changes
diff --git a/packages/react-start/README.md b/packages/react-start/README.md
index 4ead3764b3..e614561c15 100644
--- a/packages/react-start/README.md
+++ b/packages/react-start/README.md
@@ -2,7 +2,21 @@
# TanStack React Start
-
+
+
+
+
+
SSR, Streaming, Server Functions, API Routes, bundling and more powered by [TanStack Router](https://tanstack.com/router) and Vite. Ready to deploy to your favorite hosting provider.
diff --git a/packages/react-start/package.json b/packages/react-start/package.json
index c015ecfcf2..9abee3a86e 100644
--- a/packages/react-start/package.json
+++ b/packages/react-start/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/react-start",
- "version": "1.168.27",
+ "version": "1.168.36",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -188,7 +188,7 @@
}
},
"devDependencies": {
- "@rsbuild/core": "^2.0.11",
+ "@rsbuild/core": "^2.1.0",
"@types/node": ">=20"
}
}
diff --git a/packages/react-start/skills/_artifacts/domain_map.yaml b/packages/react-start/skills/_artifacts/domain_map.yaml
index 297f9ccf04..4e790903a6 100644
--- a/packages/react-start/skills/_artifacts/domain_map.yaml
+++ b/packages/react-start/skills/_artifacts/domain_map.yaml
@@ -1,13 +1,13 @@
# domain_map.yaml
# Generated by skill-domain-discovery
# Library: TanStack Start
-# Version: 1.166.2
+# Version: 1.168.32
# Date: 2026-03-07
# Status: reviewed
library:
name: '@tanstack/react-start'
- version: '1.166.2'
+ version: '1.168.32'
repository: 'https://github.com/TanStack/router'
description: >-
Full-stack React framework built on TanStack Router and Vite. Adds
@@ -189,6 +189,15 @@ skills:
priority: HIGH
status: active
+ - mistake: 'Not using useServerFn for component calls'
+ mechanism: >-
+ Component calls need useServerFn so redirects and not-found
+ responses use the active router instead of falling through as
+ ordinary function results.
+ source: 'docs/start/framework/react/guide/server-functions.md'
+ priority: MEDIUM
+ status: active
+
- mistake: 'Generating Next.js or Remix server patterns'
mechanism: >-
Agents generate getServerSideProps, "use server" directives,
@@ -204,6 +213,39 @@ skills:
priority: CRITICAL
status: active
+ - mistake: 'Relying on a route guard to protect a server function'
+ mechanism: >-
+ beforeLoad protects route UX, but createServerFn exposes an
+ independently callable endpoint. Private handlers must enforce
+ authentication and authorization themselves or through middleware.
+ source: 'protocol-v4 evaluation'
+ priority: CRITICAL
+ status: active
+
+ - mistake: 'Self-fetching a relative API URL from an SSR loader'
+ mechanism: >-
+ Loaders also run on the server, where relative URLs may not have a
+ base. App-internal loaders should call a server function directly.
+ source: 'protocol-v4 evaluation'
+ priority: CRITICAL
+ status: active
+
+ - mistake: 'Mutating without invalidating cached loader data'
+ mechanism: >-
+ Local state can show a write that was not persisted or leave route
+ cache stale. Await the write, invalidate, and verify a fresh reload.
+ source: 'protocol-v4 evaluation'
+ priority: HIGH
+ status: active
+
+ - mistake: 'Treating typecheck as proof of output schema propagation'
+ mechanism: >-
+ A typed model can still be projected or serialized without the new
+ field. Assert the actual handler or HTTP payload at runtime.
+ source: 'protocol-v4 evaluation'
+ priority: CRITICAL
+ status: active
+
# ── Middleware and Context ───────────────────────────────────────
- name: 'Middleware'
slug: 'middleware'
@@ -354,6 +396,23 @@ skills:
priority: CRITICAL
status: active
+ - mistake: 'Hydration mismatches from env-dependent rendering'
+ mechanism: >-
+ Rendering different output from server-only environment state
+ causes the client hydration pass to disagree with the SSR HTML.
+ Transfer stable data or defer environment-dependent UI.
+ source: 'docs/start/framework/react/guide/execution-model.md'
+ priority: HIGH
+ status: active
+
+ - mistake: 'Using a relative URL in an isomorphic loader'
+ mechanism: >-
+ Browser fetch resolves relative URLs against the document, while an
+ SSR runtime may have no base URL. Use a server function boundary.
+ source: 'protocol-v4 evaluation'
+ priority: CRITICAL
+ status: active
+
# ── Server Routes ────────────────────────────────────────────────
- name: 'Server Routes'
slug: 'server-routes'
@@ -387,6 +446,39 @@ skills:
priority: MEDIUM
status: active
+ - mistake: 'Forgetting to await request body methods'
+ mechanism: >-
+ Request body readers return promises. Using request.json(),
+ request.text(), or request.formData() without await passes a promise
+ instead of the parsed request body.
+ source: 'docs/start/framework/react/guide/server-routes.md'
+ priority: MEDIUM
+ status: active
+
+ - mistake: 'Relying on page auth to protect a server route'
+ mechanism: >-
+ API handlers are directly callable and must authenticate and
+ authorize private reads and writes at the handler boundary.
+ source: 'protocol-v4 evaluation'
+ priority: CRITICAL
+ status: active
+
+ - mistake: 'Self-fetching a server route from an SSR loader'
+ mechanism: >-
+ A relative API URL can fail during SSR. Share one server-side service
+ between a server function and server route instead.
+ source: 'protocol-v4 evaluation'
+ priority: CRITICAL
+ status: active
+
+ - mistake: 'Omitting a field from serialized response output'
+ mechanism: >-
+ Typechecking does not inspect the runtime Response payload. Test the
+ serialized output when a schema changes.
+ source: 'protocol-v4 evaluation'
+ priority: CRITICAL
+ status: active
+
# ── Deployment and Rendering ─────────────────────────────────────
- name: 'Deployment'
slug: 'deployment'
diff --git a/packages/react-start/skills/_artifacts/skill_spec.md b/packages/react-start/skills/_artifacts/skill_spec.md
index ce016f4e03..f550196da8 100644
--- a/packages/react-start/skills/_artifacts/skill_spec.md
+++ b/packages/react-start/skills/_artifacts/skill_spec.md
@@ -18,10 +18,10 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite
| Skill | Type | Domain | What it covers | Failure modes |
| ------------------- | --------- | ------------------------ | ------------------------------------------------------- | ------------- |
| start-setup | core | project-setup | tanstackStart(), getRouter(), root route, entries | 3 |
-| server-functions | core | server-functions | createServerFn, validation, useServerFn, streaming | 4 |
+| server-functions | core | server-functions | createServerFn, validation, useServerFn, streaming | 8 |
| middleware | core | middleware-and-context | createMiddleware, context, global middleware, factories | 3 |
-| execution-model | core | execution-model | Isomorphic defaults, environment functions, env vars | 4 |
-| server-routes | core | server-routes | server property, HTTP handlers, createHandlers | 2 |
+| execution-model | core | execution-model | Isomorphic defaults, environment functions, env vars | 5 |
+| server-routes | core | server-routes | server property, HTTP handlers, createHandlers | 5 |
| deployment | core | deployment-and-rendering | Hosting, SSR modes, prerendering, SEO | 3 |
| server-components | sub-skill | deployment-and-rendering | React Server Components, cache ownership, selective SSR | 3 |
| react-start | framework | project-setup | React bindings, useServerFn, full setup | 3 |
@@ -37,14 +37,18 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite
| 2 | Enabling verbatimModuleSyntax in tsconfig | HIGH | docs/build-from-scratch |
| 3 | Missing Scripts component in root route | HIGH | docs/guide/routing |
-### server-functions (4 failure modes)
+### server-functions (8 failure modes)
-| # | Mistake | Priority | Source |
-| --- | --------------------------------------------------------------------------- | -------- | --------------------------- |
-| 1 | Putting server-only code in loaders instead of server functions | CRITICAL | maintainer interview |
-| 2 | Generating Next.js/Remix server patterns ("use server", getServerSideProps) | CRITICAL | maintainer interview |
-| 3 | Using dynamic imports for server functions | HIGH | docs/guide/server-functions |
-| 4 | Not using useServerFn for component calls | MEDIUM | docs/guide/server-functions |
+| # | Mistake | Priority | Source |
+| --- | --------------------------------------------------------------- | -------- | --------------------------- |
+| 1 | Putting server-only code in loaders instead of server functions | CRITICAL | maintainer interview |
+| 2 | Using dynamic imports for server functions | HIGH | docs/guide/server-functions |
+| 3 | Not using useServerFn for component calls | MEDIUM | docs/guide/server-functions |
+| 4 | Generating Next.js or Remix server patterns | CRITICAL | maintainer interview |
+| 5 | Relying on a route guard to protect a server function | CRITICAL | protocol-v4 evaluation |
+| 6 | Self-fetching a relative API URL from an SSR loader | CRITICAL | protocol-v4 evaluation |
+| 7 | Mutating without invalidating cached loader data | HIGH | protocol-v4 evaluation |
+| 8 | Treating typecheck as proof of output schema propagation | CRITICAL | protocol-v4 evaluation |
### middleware (3 failure modes)
@@ -54,7 +58,7 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite
| 2 | Confusing request vs server function middleware | MEDIUM | docs/guide/middleware |
| 3 | Wrong middleware method order | MEDIUM | docs/guide/middleware |
-### execution-model (4 failure modes)
+### execution-model (5 failure modes)
| # | Mistake | Priority | Source |
| --- | ------------------------------------------------- | -------- | -------------------------------- |
@@ -62,13 +66,17 @@ TanStack Start is a full-stack React framework built on TanStack Router and Vite
| 2 | Exposing secrets via module-level process.env | CRITICAL | docs/guide/execution-model |
| 3 | Using VITE\_ prefix for server secrets | CRITICAL | docs/guide/environment-variables |
| 4 | Hydration mismatches from env-dependent rendering | HIGH | docs/guide/execution-model |
+| 5 | Using a relative URL in an isomorphic loader | CRITICAL | protocol-v4 evaluation |
-### server-routes (2 failure modes)
+### server-routes (5 failure modes)
-| # | Mistake | Priority | Source |
-| --- | ---------------------------------------- | -------- | ------------------------ |
-| 1 | Duplicate route path resolution | MEDIUM | docs/guide/server-routes |
-| 2 | Forgetting to await request body methods | MEDIUM | docs/guide/server-routes |
+| # | Mistake | Priority | Source |
+| --- | ------------------------------------------------ | -------- | ------------------------ |
+| 1 | Duplicate path resolution for server routes | MEDIUM | docs/guide/server-routes |
+| 2 | Forgetting to await request body methods | MEDIUM | docs/guide/server-routes |
+| 3 | Relying on page auth to protect a server route | CRITICAL | protocol-v4 evaluation |
+| 4 | Self-fetching a server route from an SSR loader | CRITICAL | protocol-v4 evaluation |
+| 5 | Omitting a field from serialized response output | CRITICAL | protocol-v4 evaluation |
### deployment (3 failure modes)
diff --git a/packages/react-start/skills/_artifacts/skill_tree.yaml b/packages/react-start/skills/_artifacts/skill_tree.yaml
index 21f8c57c3a..3078228f73 100644
--- a/packages/react-start/skills/_artifacts/skill_tree.yaml
+++ b/packages/react-start/skills/_artifacts/skill_tree.yaml
@@ -1,7 +1,7 @@
# skills/_artifacts/start_skill_tree.yaml
library:
name: '@tanstack/react-start'
- version: '1.166.2'
+ version: '1.168.32'
repository: 'https://github.com/TanStack/router'
description: >-
Full-stack React framework built on TanStack Router and Vite.
diff --git a/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md b/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md
index c9c098addf..d59292bc4b 100644
--- a/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md
+++ b/packages/react-start/skills/lifecycle/migrate-from-nextjs/SKILL.md
@@ -1,13 +1,14 @@
---
-name: lifecycle/migrate-from-nextjs
+name: migrate-from-nextjs
description: >-
Step-by-step migration from Next.js App Router to TanStack Start:
route definition conversion, API mapping, server function
conversion from Server Actions, middleware conversion, data
fetching pattern changes.
-type: lifecycle
-library: tanstack-start
-library_version: '1.166.2'
+metadata:
+ type: lifecycle
+ library: tanstack-start
+ library_version: '1.168.32'
requires:
- start-core
- react-start
diff --git a/packages/react-start/skills/react-start/SKILL.md b/packages/react-start/skills/react-start/SKILL.md
index c8dbfd4bbf..b0f69009e1 100644
--- a/packages/react-start/skills/react-start/SKILL.md
+++ b/packages/react-start/skills/react-start/SKILL.md
@@ -5,10 +5,11 @@ description: >-
StartServer, React-specific imports, re-exports from
@tanstack/react-router, full project setup with React, useServerFn
hook.
-type: framework
-library: tanstack-start
-library_version: '1.166.2'
-framework: react
+metadata:
+ type: framework
+ library: tanstack-start
+ library_version: '1.168.32'
+ framework: react
requires:
- start-core
sources:
@@ -18,9 +19,7 @@ sources:
# React Start (`@tanstack/react-start`)
-This skill builds on start-core. Read [start-core](../../../start-client-core/skills/start-core/SKILL.md) first for foundational concepts.
-
-This skill covers the React-specific bindings, setup, and patterns for TanStack Start.
+This is the React Start entry skill. Use the workflow below, then load only the package skill that owns the boundary you are changing. Do not read `start-core`, Router Core, and React Router manuals in full before starting.
For React Server Components patterns, see [react-start/server-components](./server-components/SKILL.md).
@@ -30,6 +29,16 @@ For React Server Components patterns, see [react-start/server-components](./serv
> **CRITICAL**: Types are FULLY INFERRED. Never cast, never annotate inferred values.
+## Full-Stack Workflow
+
+1. Define the route and component with `createFileRoute`.
+2. Put private or server-only reads and writes in `createServerFn`; call reads directly from loaders.
+3. Use `useServerFn` for component mutations, then invalidate the router or query cache after the write resolves.
+4. Enforce auth in every private server function or server route. Add `beforeLoad` separately for navigation UX.
+5. Run the initial SSR path, client navigation, mutation plus reload, direct anonymous endpoint request, runtime response assertion, type tests, and production build.
+
+Load `start-core/server-routes` instead of `server-functions` only when a raw HTTP endpoint is required. Load `router-core/*` only for the specific routing concern involved, such as params or search validation.
+
## Package API Surface
`@tanstack/react-start` re-exports everything from `@tanstack/start-client-core` plus:
diff --git a/packages/react-start/skills/react-start/server-components/SKILL.md b/packages/react-start/skills/react-start/server-components/SKILL.md
index eda716f32e..1d0b0b099f 100644
--- a/packages/react-start/skills/react-start/server-components/SKILL.md
+++ b/packages/react-start/skills/react-start/server-components/SKILL.md
@@ -1,5 +1,5 @@
---
-name: react-start/server-components
+name: server-components
description: >-
Implement, review, debug, and refactor TanStack Start React Server
Components in React 19 apps. Use when tasks mention
@@ -12,9 +12,10 @@ description: >-
migration from Next App Router RSC patterns. Do not use for
generic SSR or non-TanStack RSC frameworks except brief
comparison.
-type: sub-skill
-library: tanstack-start
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-start
+ library_version: '1.168.32'
requires:
- react-start
- start-core/server-functions
diff --git a/packages/router-cli/CHANGELOG.md b/packages/router-cli/CHANGELOG.md
index 7895121dbd..8b9c4c1d71 100644
--- a/packages/router-cli/CHANGELOG.md
+++ b/packages/router-cli/CHANGELOG.md
@@ -1,5 +1,33 @@
# @tanstack/router-cli
+## 1.167.22
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/router-generator@1.167.22
+
+## 1.167.21
+
+### Patch Changes
+
+- Updated dependencies [[`78dd1a6`](https://github.com/TanStack/router/commit/78dd1a645dc1a9e9f4f649d9aff12005044d4fcc)]:
+ - @tanstack/router-generator@1.167.21
+
+## 1.167.20
+
+### Patch Changes
+
+- Updated dependencies [[`e56a677`](https://github.com/TanStack/router/commit/e56a67742da9021b009b8db0cdc8bfe99878c25b)]:
+ - @tanstack/router-generator@1.167.20
+
+## 1.167.19
+
+### Patch Changes
+
+- Updated dependencies []:
+ - @tanstack/router-generator@1.167.19
+
## 1.167.18
### Patch Changes
diff --git a/packages/router-cli/package.json b/packages/router-cli/package.json
index 50aa6f8d4b..313869c287 100644
--- a/packages/router-cli/package.json
+++ b/packages/router-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/router-cli",
- "version": "1.167.18",
+ "version": "1.167.22",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -27,12 +27,12 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"build": "vite build"
},
diff --git a/packages/router-core/CHANGELOG.md b/packages/router-core/CHANGELOG.md
index a0082099fa..52db7369b6 100644
--- a/packages/router-core/CHANGELOG.md
+++ b/packages/router-core/CHANGELOG.md
@@ -1,5 +1,49 @@
# @tanstack/router-core
+## 1.171.16
+
+### Patch Changes
+
+- [#7805](https://github.com/TanStack/router/pull/7805) [`45c4ad8`](https://github.com/TanStack/router/commit/45c4ad8d629e291fab70c37900525449e415ffcd) - Rewrite match loading around a lane-based scheduler that tracks each navigation, preload, and background reload as an ordered unit of work. This fixes pending/redirect/retry state leaking between overlapping navigations, restores correct SSR status codes for redirects, errors, and not-found responses, and closes hydration gaps where the client re-ran work the server had already completed.
+ - Invalidation now retires matching active preloads so older speculative loader results cannot become fresh cache data after invalidation.
+ - Route `headers()` now only runs on the server, matching the documented behavior — it is no longer invoked during client-side asset projection.
+ - The documented default `gcTime` and `preloadGcTime` now match the existing runtime default of 5 minutes (`300_000`).
+
+ **Removed / changed exported internals**
+ - `RouterState` no longer includes `loadedAt`, `isTransitioning`, `statusCode`, or `redirect`. Use `match.updatedAt` in place of `loadedAt`; subscribe to `router.state.status` / `router.state.isLoading` in place of `isTransitioning`; server response status and redirect handling are now internal to the server loader and are no longer exposed on `router.state`.
+ - `RouteMatch.fetchCount` has been removed, with no replacement — it was purely informational.
+ - `RouteMatch.status` no longer includes `'redirected'` (it remains `'pending' | 'success' | 'error' | 'notFound'`) — redirected matches are dropped from the match list instead of being rendered.
+ - `RouteMatch.globalNotFound` has been renamed and privatized to the internal `_notFound` field. Use `match.status === 'notFound'` instead.
+ - The exported React, Solid, and Vue `Match` components now accept `routeId` instead of `matchId`.
+ - The exported `RouterStores` adapter contract now uses route-keyed presentation stores: `matchesId` is replaced by `ids`, `matchStores` by `byRoute`, and `getRouteMatchStore()` by `getMatchStore()`. The separate `loadedAt`, `isLoading`, `isTransitioning`, `statusCode`, and `redirect` stores have been removed, along with the pending/cache stores and their setters. `StoreConfig.init` has also been removed. Read application-facing state from `router.state`; preload and cache coordination are now internal.
+ - Removed `RouterCore` members `getMatch()`, `updateMatch()`, `cancelMatch()`, and `cancelMatches()` — read matches from `router.state.matches` (e.g. `router.state.matches.find((m) => m.id === id)`); there is no replacement for mutating or cancelling an individual in-flight match from outside the router.
+ - Removed `RouterCore.hasNotFoundMatch()` — use `router.state.matches.some((m) => m.status === 'notFound')`.
+ - Removed `RouterCore.looseRoutesById` — use `routesById`.
+ - Removed `RouterCore.isPrerendering()`, `RouterCore.isViewTransitionTypesSupported`, and `RouterCore.viewTransitionPromise`, with no replacement.
+ - Removed `RouterCore.getParsedLocationHref()` and `RouterCore.clearExpiredCache()`, with no replacement — expired cache entries are now reconciled automatically as part of match commit.
+ - Removed `RouterCore.latestLoadPromise` and `RouterCore.beforeLoad()`, with no replacement.
+ - `RouterCore.commitLocationPromise` and `RouterCore.pendingBuiltLocation` have been replaced by the internal `_commitPromise` and `_pendingLocation` fields.
+ - Removed the exported `GetMatchFn` and `UpdateMatchFn` types, along with the methods they typed.
+ - Removed the standalone `getMatchedRoutes()` export from `@tanstack/router-core` — use the `router.getMatchedRoutes()` instance method instead.
+ - `RouterCore.loadRouteChunk()` no longer accepts an array of component types as its second argument. One-argument usage is unchanged; the optional second argument is now `'errorComponent'`, `'notFoundComponent'`, or `false` for internal boundary loading.
+ - Removed `Redirect.redirectHandled`, which was internal redirect bookkeeping.
+ - `MatchRoutesOpts.preload` and `MatchRoutesOpts.dest` have been removed.
+ - `StartTransitionFn` is now `(fn, expected) => Promise` (previously `(fn) => void`). This only affects custom framework adapters that implement `startTransition`.
+
+## 1.171.15
+
+### Patch Changes
+
+- [#7807](https://github.com/TanStack/router/pull/7807) [`e2dd204`](https://github.com/TanStack/router/commit/e2dd2049cb42eb219d3b447b8605066d19d9c1fa) - fix(router-core): handle window and element scroll restoration independently
+
+ Window and element scroll targets are now handled independently. Restoring one target no longer suppresses resets for other uncached configured targets, and a restored element is no longer reset when the window has no cached position.
+
+ Hash navigation no longer resets elements configured through `scrollToTopSelectors` and retains precedence over stale window positions through destination invalidations.
+
+ Scroll positions are sampled when leaving a route, preserving live changes made after the most recent scroll event. This also prevents client hydration from undoing nested positions restored by the SSR script.
+
+ Fixes [#7687](https://github.com/TanStack/router/issues/7687).
+
## 1.171.14
### Patch Changes
diff --git a/packages/router-core/INTERNALS.md b/packages/router-core/INTERNALS.md
new file mode 100644
index 0000000000..0335da541a
--- /dev/null
+++ b/packages/router-core/INTERNALS.md
@@ -0,0 +1,1317 @@
+# Match loading internals
+
+This document describes the architecture that loads route matches on the
+client, on the server, and across hydration. It is for maintainers of router
+core and the framework adapters.
+
+Match loading coordinates several asynchronous flows, but its runtime
+coordination state is limited to actual writers and owned resources. Phase names
+and invalid transitions belong in TypeScript whenever possible; they do not
+justify a second runtime state machine.
+
+All `_`-prefixed fields mentioned here are internal. Their spelling and shape
+may change, but the ownership rules in this document must continue to hold.
+
+## Vocabulary and the short version
+
+- A **match** is one route at one concrete set of path params, search-derived
+ loader dependencies, and loader/cache identity.
+- A **lane** is one location plus its ordered array of matches.
+- A **generation** is one concrete match result for a match ID. Several
+ generations can share an ID while holding different loader data or ownership;
+ a newer one can exist without being the one currently displayed.
+- A **loader flight** is one loader invocation together with its promise and
+ abort controller. `router._flights` lets later consumers find the newest
+ same-ID flight.
+- A **lease** is one match's ownership of a flight. Registry membership makes a
+ flight discoverable; leases keep it alive.
+- **Semantic state** is the accepted lane used for reuse and lifecycle
+ decisions. **Presentation state** is the lane currently exposed to rendering
+ and selectors.
+- A **cutoff** is the end of the match prefix allowed to render or contribute a
+ particular output. The first pending or terminal boundary normally sets it,
+ even though later matches remain structurally present.
+- To **publish** is to replace router state that renderers or users can observe.
+ An **identity check** means that publication is allowed only while the exact
+ owner or base reference used to produce the result is still current.
+
+The normal client flow is:
+
+```text
+match a private lane
+ -> build context and run beforeLoad parent-first
+ -> run eligible loaders and component readiness work
+ -> select one final outcome and derive assets
+ -> publish only if the navigation still owns the result
+```
+
+Preloads run the same work without publishing a lane. Server loading uses a
+request signal instead of a client navigation owner. Hydration reconstructs the
+accepted server prefix, then hands any remaining client work to the initial
+client load.
+
+## The architectural rule
+
+For each mutable result, there must be one answer to each of these questions:
+
+1. Who may publish it?
+2. Who owns the work and cancellation signal that produce it?
+3. What proves that the answer is still current after an `await`?
+
+Do not add a flag, counter, copied deadline, or second completion promise merely
+to describe an existing fact. Add runtime state only when it removes an
+independent writer, resource owner, or invalid transition.
+
+The main authorities are:
+
+| Authority | What it owns |
+| ---------------------------- | ------------------------------------------------------------------- |
+| `_tx` | The client navigation allowed to commit and publish foreground flow |
+| `_preflight` | The current client plan or asynchronous hydration reconstruction |
+| `_handoff` | The temporary right to transfer one reconstructed SSR prefix |
+| `_committed` | The accepted current lane and lifecycle/background identity base |
+| `stores.matches` | The current match presentation exposed to renderers and users |
+| `router._cache` | Off-screen loader generations preferred during same-ID planning |
+| Active preload entry | Cancellation, cache clearing, and private redirect-chain ownership |
+| Loader flight registry entry | The latest same-ID loader generation available to new consumers |
+| Match flight lease | Ownership keeping that loader work alive |
+| Pending session | One reveal/minimum-visible deadline and its current owner |
+| React acknowledgement slot | The one requested publication whose render may settle a transition |
+| Refresh transaction | Its starting presentation, handoff, and ability to roll back |
+| Request signal | Lifetime of one server request and any accepted SSR stream |
+| Accepted SSR stream response | Cleanup ownership transferred from the handler to the response body |
+
+These authorities are related, but none is a substitute for another. In
+particular, presentation is not semantic authority, registry membership is not
+resource ownership, and a promise settling is not permission to publish.
+
+## Code map
+
+- `src/router.ts` matches locations, creates match objects, owns public router
+ state, history, cache operations, and entry points into loading.
+- `src/stores.ts` reconciles route-keyed presentation stores and their ordered
+ aggregate.
+- `src/load-client.ts` owns client planning, transactions, preloads, loader
+ flights, lazy route and component readiness, reduction, pending presentation,
+ background reloads, commits, and hydration reconstruction.
+- `src/load-server.ts` runs the request-local server lane.
+- `src/ssr/createRequestHandler.ts` connects request lifetime, server loading,
+ dehydration, redirects, rendering, and cleanup.
+- `src/ssr/handlerCallback.ts`, `src/ssr/ssr-server.ts`, and
+ `src/ssr/transformStreamWithRouter.ts` transfer stream ownership and
+ coordinate serialization, injection, abort, and cleanup.
+- Framework `Transitioner` and `Matches` implementations acknowledge exact
+ publications and render only through the selected boundary. Framework
+ `RouterClient` and render-to-stream implementations complete hydration and
+ connect request abort to their renderer.
+
+## Semantic state and presentation state
+
+The rewrite deliberately separates two views of matches.
+
+### Semantic matches
+
+`_committed` is the accepted current semantic lane. It supplies lifecycle
+identity and is the exact base checked before a background publication. Pending
+presentation never replaces it as a planning base.
+
+For an individual match ID, matching first consults `router._cache` and then
+falls back to `_committed`. A cached loader generation can therefore shadow the
+currently displayed same-ID generation for future planning without becoming
+current presentation or lifecycle authority.
+
+Semantic matches may own loader-flight leases. They are not mutated by losing
+transactions or by pending presentation.
+
+### Presented matches
+
+`stores.matches` is the array visible to router state and framework stores.
+Before `pendingMs` expires it can still contain the source presentation. Once
+pending presentation is published, it contains the whole destination lane,
+including descendants that are still loading. Terminal publication follows the
+same membership rule: an error is retained at the throwing match, while
+not-found is moved to its selected boundary, but neither removes
+structurally matched descendants. The framework renderer derives its cutoff
+from the first pending or terminal boundary instead of requiring core to hide
+descendants.
+
+The render cutoff also bounds route response headers, SSR manifest assets,
+dehydration, and cache exclusion during commit. Framework construction of head
+and scripts uses the closely related asset cutoff. It normally stops at the
+same boundary, with one selective-SSR hydration exception: a verified
+`'data-only'` prefix can carry `_assetEnd`, allowing already-projected assets
+from verified descendants to remain active past a pending render boundary until
+the client continuation commits. A structural descendant below the relevant
+cutoff remains observable in state, but it cannot contribute that output or
+evict a newer cache generation merely because it is present in the lane.
+
+Start's static early hints are another deliberate exception. They are
+speculative route-tree hints emitted before loading selects a terminal boundary,
+and an already-sent 103 response cannot be retracted.
+
+The presentation pool is keyed by route ID, not match ID. `stores.ids` defines
+active membership and order, while `stores.getMatchStore` obtains the one stable
+mutable atom in `stores.byRoute` for a route. That atom contains the route's
+presented match or `undefined`. An active route branch contains a route at most
+once, so this shape cannot alias two visible matches. When params, search, or
+loader dependencies produce a new semantic match ID for the same route,
+reconciliation replaces the value in that route's existing atom. Leaving a
+route tombstones that atom with `undefined`; re-entering fills the same atom
+again. Route components and `useMatch({ from })` therefore keep one subscription
+across match generations and A-to-B-to-A membership changes. `ids` is published
+before departure tombstones so framework trees stop reading a leaving route
+before its atom is cleared. The pool retains atoms for route IDs encountered
+during the router's lifetime; it is not an LRU or a cache of match generations.
+Semantic caches and loader flights remain keyed by match ID and must not use the
+presentation pool as their authority.
+
+This distinction matters for user code. Once the destination is presented, a
+selector can inspect every destination match and observe `isFetching` while the
+renderer still shows only the valid prefix. Before that publication, same-ID
+matches in the source presentation can expose updated fetching state, but a new
+destination-only match is still private.
+
+Pending entries are flight-free snapshots. They may copy loader data and context
+for presentation, but they never become a planning base and never own semantic
+resources.
+
+### Location state
+
+`stores.location` is the requested location. It can advance before loading
+finishes. For foreground client navigation, `stores.resolvedLocation` advances
+only after the framework transition acknowledgement settles. Settlement, not a
+`true` render result, is what permits navigation completion. Router `status` is
+`pending` during that interval and returns to `idle` at completion. A `true`
+result separately permits `onRendered` and pending minimum timing. Server
+publication and hydration perform their request/initial-load handoffs directly,
+as described below.
+
+### Canonical locations and rewrites
+
+Initial client and server canonicalization compares `publicHref`, the
+browser-facing URL produced by the rewrite contract. Parsed semantic `href` and
+rebuilt `href` are not necessarily symmetric: input and output rewrites run in
+opposite directions. The client ignores a trailing-slash-only difference while
+the server can redirect to the exact browser-facing canonical URL.
+
+## A lane and its phases
+
+A lane is one location plus an ordered array of work matches. Its phase is
+encoded in TypeScript:
+
+```text
+matched -> contextualized -> reduced -> projected
+```
+
+### Matched
+
+Matching establishes route order, params, validated search results, loader
+dependencies, match IDs, initial status, and possible semantic reuse. It does
+not run `beforeLoad` or grant publication authority.
+
+A match ID identifies loader/cache compatibility. It is derived from route
+identity, interpolated path params, and serialized `loaderDeps`. Other search
+values affect loader identity only when the route includes them in `loaderDeps`.
+
+Matching treats `params.parse`, `validateSearch`, and `loaderDeps` as pure
+planning functions. For the same input they must return the same value without
+navigating or mutating router/application state. A `loaderDeps` result and its
+serialization hooks must also be side-effect-free. These callbacks may be
+evaluated more than once; supporting reentrancy from them would add runtime
+ownership checks to every planning step without representing a supported use.
+
+### Contextualized
+
+Contextualization walks parent to child. For each route it:
+
+1. computes route context from the completed parent context,
+2. handles params/search validation,
+3. runs the route's `beforeLoad`, when defined, and
+4. merges the result before moving to the child.
+
+This serial order guarantees that child route context, child `beforeLoad`, and
+child loader context cannot observe a partially completed parent guard.
+
+Route context is synchronous in the public type contract. Its own contribution
+is cached on the match as `_ctx`, and match identity includes route identity,
+path params, and `loaderDeps`. A same-ID cache hit may therefore reuse that
+route-local result, including one produced by a completed preload. The merged
+context is still rebuilt parent-first from the current parent's merged context,
+the route's `_ctx`, and the current `beforeLoad` result. Reusing `_ctx` must never
+reuse an older merged context or `beforeLoad` contribution.
+
+Server requests have fresh matches and execute route context normally.
+Hydration executes each accepted route context locally, stores its `_ctx`, and
+then merges transported `beforeLoad` output. Every normal client lane,
+including every preload, performs its own contextualization and `beforeLoad`
+chain.
+
+### Reduced
+
+On the client, eligible loaders and normal component chunks start concurrently.
+On the server, loaders reduce before normal render chunks are consumed.
+Reduction turns their outcomes into one terminal semantic lane and one cutoff.
+No task publishes while reduction is in progress.
+
+### Projected
+
+After semantic reduction, client asset hooks derive `meta`, `links`, styles, and
+scripts from the final lane. Server projection additionally derives response
+headers. Projection cannot replace the selected loader/before-load result.
+
+Only an owner that remains current after projection may publish.
+
+## Client planning and the single writer
+
+Planning is intentionally separate from transaction installation. Lifecycle
+events and route execution callbacks can synchronously reenter the router.
+Planning callbacks used for params, search, and loader-key derivation are the
+pure functions described above and do not create a second reentrancy boundary.
+
+A planning controller is installed before `onBeforeNavigate`, `onBeforeLoad`,
+and matching, invalidating an older synchronous plan. The planner checks its
+authority after supported reentrant callbacks and before installing a
+transaction. A stale plan exits without installing a transaction or altering
+semantic state.
+
+Once planning succeeds, the router installs one `_tx`. The transaction owns:
+
+- the destination and its private matches,
+- one lane cancellation controller,
+- `done`, the transaction-completion promise used by current transaction
+ waiters and by React to retain the Suspense source tree while a published
+ pending match is still loading,
+- redirect depth for the chain currently being executed, and
+- any pending session transferred to it.
+
+`LoadTransaction`, the lane execution options, and the pending session are
+labeled TypeScript tuples. They are deliberately not runtime state machines:
+the labels make each slot type-safe for maintainers, while the compact runtime
+shape avoids repeating property names throughout the client loader. Changing a
+slot means updating the tuple declaration and every typed consumer together;
+it must not introduce a second owner or completion signal.
+
+Throwing `done` from a pending React match does not stop the transaction's work.
+Once the lane has reduced and projected, its successful destination is
+published inside the framework transition; that render acknowledgement then
+allows `done` to settle. `_commitPromise` is the internal promise backing public
+history/navigation completion. Current completion resolves it, and a
+superseding transaction can chain it forward, but it is also not permission to
+publish. These promises describe different wait relationships; `_tx` remains
+the only client writer authority.
+
+Redirect depth transfers only through the exact `_pendingLocation` created by
+`followRedirect`. It is not inherited from the previous transaction: a user
+navigation that reenters during a redirect starts a fresh chain even though the
+redirecting transaction is still alive.
+
+The latest `_tx` remains installed after it settles. Its presence is writer
+identity, not a loading flag; public loading state comes from router `status`
+and per-match `isFetching`.
+
+Installing a successor removes the predecessor's publication authority. The
+predecessor must still settle and release everything it owns; cancellation is a
+liveness mechanism, not a license to abandon cleanup.
+
+Commit and cache handoff install the accepted semantic/cache recipients before
+releasing replaced resources. Releasing the last flight lease aborts a public
+signal and can synchronously reenter user code, so an old generation must never
+be released while it still appears to be the accepted owner. The committed lane
+is also removed from the transaction's private ownership before publication.
+
+Every asynchronous navigation or presentation publication checks `_tx`
+immediately before the write. Background publication additionally checks the
+exact committed base array from which it was derived. Preload cache admission
+and private redirect continuation use their own identity and controller checks
+instead.
+
+## `beforeLoad`: execution and hydration
+
+`beforeLoad` context is not a cache.
+
+A completed client preload never stores reusable `beforeLoad` output. When its
+loader data enters the route cache, the merged context is discarded; the
+same-ID route-local `_ctx` may remain reusable. A later navigation rebuilds the
+merged context from the current parent and `_ctx`, then reruns `beforeLoad`, even
+when it reuses completed loader data.
+
+There is one deliberate exception: hydration.
+
+### Client lanes
+
+Every navigation and every preload rematches and runs its own serial
+`beforeLoad` chain. This remains true for identical concurrent preloads and for
+a navigation that targets a still-running preload. A call made with
+`preload: true` is never accepted as the navigation's call with
+`preload: false`; that call's context and control or terminal outcome stay
+private to its speculative lane. This does not isolate the normalized outcome of
+a same-ID loader flight that multiple lanes deliberately share.
+
+`router._preloads` has no semantic adoption role. Its controller entry owns
+cancellation, invalidation, and cache clearing and proves that a standalone
+preload remained active through normal settlement; successful removal authorizes
+private redirect continuation. Its live lane signal is also the final authority
+for cache publication, including the microtask window after a loader outcome has
+fulfilled. Loader results remain independently reusable: a completed preload may
+seed the match cache, and a still-running preload may donate its same-ID loader
+flight after the receiving lane has run its own `beforeLoad` and made its reload
+decision.
+
+### Hydration
+
+The server-resolved prefix is authoritative for the initial document. Hydration
+therefore restores transported `beforeLoad` output for the accepted prefix
+without rerunning it on the client. This applies to the server-rendered prefix
+in selective SSR; the unresolved client suffix follows normal navigation
+rules.
+
+Hydration is not a general `beforeLoad` cache. Its temporary handoff is valid
+only for the initial client load of the same document entry and exact committed
+owner; rejection, invalidation, or any later load returns to normal serial
+execution. The claim also requires the same raw browser href and history-state
+object. Finish-time match-ID validation proves that rematching still produced
+the accepted prefix; opaque router context and route-tree objects are not
+compared. That initial load may transfer the accepted hydration prefix and keep
+its transported context while it completes a selective-SSR suffix. A preload
+never claims this prefix. Frameworks must start the initial client load before
+descendant route code can preload; invoking a preload in the gap after raw
+`hydrate()` and before that load is outside the supported handoff protocol.
+
+## Loader data, cache entries, and flights
+
+Loader data is designed to be reusable. It is independent from `beforeLoad`
+provenance.
+
+### Completed cache entries
+
+Successful loader-backed matches can be cached by match ID. Staleness,
+invalidation, `shouldReload`, stale reload mode, and GC policy decide whether a
+lane uses that data or requires a loader generation. A discoverable same-ID
+flight may satisfy that requirement, including when `shouldReload` returns
+`true`.
+
+It is valid for cached loader data to have been produced under context from an
+older `beforeLoad` generation. Loaders are the cache boundary; guards are not.
+Likewise, a shared in-flight invocation may have started with another lane's
+older context. Only a newly started loader invocation receives the current
+lane's freshly built context.
+
+An invalid successful entry may remain in the cache as stale data and preserve
+its generation identity, but it can never satisfy freshness and must reload.
+Failed, canceled, loaderless, and expired generations do not become reusable
+loader-cache entries.
+
+A terminal preload lane can still contain independently successful loader
+generations when its error or not-found came from `beforeLoad`, validation, or
+another route. Each preload loader success attempts cache admission immediately,
+before whole-lane reduction. The cache receives a non-terminal copy and an
+additional flight lease; the speculative lane keeps its own lease and terminal
+meaning until it is discarded. This works even below the eventual render
+boundary and does not preserve the speculative parent chain: merged
+context and `beforeLoad` output are cleared. Same-ID `_ctx`, loader identity,
+and successful loader data remain reusable by design.
+
+Hydration retry is the transported-work exception because it did not run those
+client loader tasks. There, `loaderData` membership together with
+`invalid === false` proves a transported successful generation even when
+terminal boundary state is attached to the match. Hydration normalizes that copy
+before passing it through the same cache identity and lease rules.
+
+The dehydrated payload omits `loaderData` both when no loader result exists and
+when the accepted result is `undefined`. This deliberately keeps the HTML
+payload smaller at the cost of making those states indistinguishable after
+transport. Reconstruction preserves the absence and does not treat an omitted
+value as reusable loader success during hydration retry.
+
+The cache may deliberately contain a successful generation with the same match
+ID as a committed match. For example, a speculative lane can produce reusable
+ancestor loader data before failing below it while the older committed
+generation remains visible. Cache-first matching lets the next lane use that
+newer loader generation. Its merged context and `beforeLoad` contribution have
+been removed; the merged chain is rebuilt from current parents and same-ID
+route-local context before `beforeLoad` reruns.
+
+Commit removes a cache entry when the accepted render prefix contains that ID,
+or when a successful match anywhere in the committed lane contains it. A
+non-success descendant below the render cutoff is only structural membership;
+it must not evict a newer same-ID cache generation.
+
+### Same-ID in-flight work
+
+`router._flights` is the only registry from which a new consumer discovers
+same-ID loader work. Every new loader generation registers there, whether it
+was started by navigation, preload, or background refresh. A newer generation
+replaces the registry entry synchronously. A flight has its own abort
+controller; it does not use one consumer transaction's controller as its
+lifetime.
+
+Two facts must remain separate:
+
+- registry membership means new consumers may join the flight;
+- a match lease means an existing consumer keeps the flight alive.
+
+Registry membership normally owns no lease. A successfully settled generation
+may remain discoverable while at least one semantic or cached match owns it. A
+non-success outcome removes the exact current flight from the registry as it
+settles; existing leases keep that outcome alive only for consumers that already
+joined, while later planners start a fresh generation. Releasing the last lease
+removes a successful generation only if it is still the current registry entry,
+then aborts its controller. Every copied semantic match that retains a flight
+must acquire a lease, and every discarded match must release one exactly once.
+
+There is one short exception to normal zero-lease cleanup. When one navigation
+replaces another with the same match ID, the predecessor's loader flight can
+reach zero leases before the successor finishes `beforeLoad` and decides whether
+to reuse it. While the current transaction is visibly running `beforeLoad` for
+that same-ID successor, the flight remains discoverable. The same grace period
+applies if any other same-ID flight loses its final lease during that phase.
+Outside this phase, merely having a current `_tx` is not enough to retain a
+zero-lease flight.
+
+Loader planning ends the grace period synchronously. The successor either
+acquires the discoverable flight or one sweep removes and aborts it. A lane with
+no `beforeLoad` reaches loader planning before contextualization yields; an
+asynchronous `beforeLoad` keeps the grace period visible until it settles.
+Preloads neither create nor sweep this navigation-only reservation. An explicit
+`shouldReload: false` declines the flight, while invalidation removes discovery
+for selected IDs before starting the replacement load. No extra flag, counter,
+or completion promise represents the grace period.
+
+Releasing a set of matches is deliberately two-phase. First every outgoing
+match drops its `_flight` lease and every zero-owner generation not reserved by
+the current transaction is removed from the discovery registry. Only after the
+entire outgoing set is detached are the collected flight controllers aborted.
+Do not interleave one flight abort with detaching later matches in the same
+replacement: an abort listener can synchronously reenter, and that load must
+observe every logically removed lease and registry entry as already gone.
+
+A successful accepted match may keep its lease after the loader promise has
+settled. This keeps the loader's public `AbortSignal` alive for that semantic
+generation. The signal aborts when the last active or cached owner is replaced,
+unloaded, expired, or discarded; promise settlement alone does not end it.
+
+Loader error normalization, including route `onError`, runs once while the exact
+flight still has a match lease. The terminal flight leaves the discovery
+registry before normalization, so a navigation reentered by `onError` starts a
+fresh generation. Releasing every match before a late rejection makes the
+generation semantically discarded; abort-triggered rejection must not call user
+error hooks. This is an ownership check, not a separate cancellation flag. A
+loader that aborts its own flight controller while its match remains owned can
+still fulfill or reject normally.
+
+A planned match holds only the lease for the accepted generation copied from
+cache or committed state. After `beforeLoad` and `shouldReload` decide that a
+loader will run, it may synchronously acquire the registry's latest different
+same-ID generation. It never reacquires its own accepted generation merely to
+avoid a requested reload. A blocking reload replaces the accepted lease with
+the donor; a background reload keeps accepted data visible and gives the donor
+lease to its private candidate. This one lookup covers work started by active
+preloads, navigation, and background refresh without scanning those owners.
+
+Active preload flights need no special handoff. Their speculative matches keep
+positive leases until the preload settles or is canceled, so another lane can
+discover and acquire the flight without adopting the preload lane.
+
+Consumers already joined to one loader flight observe its single normalized
+outcome, including error and not-found. Per-lane cancellation can stop only that
+consumer's wait. A non-success flight retires from discovery at settlement, so
+only lanes planned afterward retry. Redirect remains control flow and is never
+cached as loader data.
+
+### Semantic parent chain
+
+`parentMatchPromise` represents the semantic parent generation, not merely the
+currently displayed parent.
+
+This distinction is essential for mixed reload modes. If a parent is refreshing
+in the background while a child reloads in blocking mode, the child borrows the
+fresh parent candidate. The final lane must not combine fresh parent data with
+child data derived from the stale visible parent.
+
+The same semantic-parent chain is used by blocking and background loader work.
+Task arrays track readiness/outcomes; they are not a second parent authority.
+
+### Components and lazy route options
+
+There is no router-level component-promise cache. The browser module cache and
+framework lazy-component machinery already cache loaded JavaScript.
+
+Lazy route loading retains only the authority needed to install lazy route
+options, ignore obsolete HMR settlements, and retry failed imports. It is not a
+general JavaScript-module cache.
+
+Lazy option installation has one route-owned promise. Success installs options
+only while that promise is still the route's owner. Rejection clears the owner
+so a later load can retry, and development refresh can clear ownership so an
+obsolete import cannot install options afterward.
+
+Normal component readiness is part of route readiness, not merely an asset
+prefetch side effect. Client loader and normal component work may run in
+parallel, and a blocking match becomes successful only after both are ready.
+When a client lane installs a `pendingComponent` from lazy options, that
+component wakes pending selection once it is itself ready, without waiting for
+the normal component. This does not require retaining a component promise on
+the match: the route's lazy-option owner and the framework/module loader provide
+the necessary work identity.
+
+Client and server not-found boundary searches settle lazy options on each
+candidate route before testing for `notFoundComponent`. A lazy rejection while
+locating the boundary does not replace an already selected not-found, but
+cancellation or request abort still stops the search. The selected terminal
+boundary component is then loaded best effort.
+
+## Outcomes and failure selection
+
+Internal work normalizes returned and thrown values into a small closed set:
+
+```text
+success | error | not-found | redirect | canceled/skipped
+```
+
+Redirect and cancellation are control flow, not committed match statuses.
+Error and not-found are terminal semantic outcomes assigned once during
+reduction.
+
+Returned and thrown redirects/not-founds normalize identically. Only an error
+invokes route `onError`; if `onError` throws, its value is normalized again and
+may itself become an error, not-found, or redirect.
+Router cancellation and request abort bypass `onError`. Aborting the
+`AbortController` exposed to a loader is not by itself proof that the router
+discarded the work: a still-owned loader may fulfill or reject afterward, and
+that settlement is normalized normally. On the client, zero flight leases prove
+that an aborted generation was discarded. On the server, the request signal
+proves request cancellation, while the already selected failure/control outcome
+proves that an aborted descendant is obsolete. The client calls a discarded
+non-result `canceled`, while the server calls it `skipped`; neither is a
+publishable terminal state.
+
+The client and server use the same settlement order and renderable-ancestor
+rules.
+
+### Serial phase
+
+Route context, validation, and `beforeLoad` run parent-first. The first terminal
+serial outcome stops descent and wins over later loader or chunk work when the
+ancestors needed to render its boundary remain usable.
+
+An error from the serial phase allows loaders strictly above the throwing route
+to finish. A serial not-found allows work through its effective ancestor
+boundary, but never past the throwing route. A serial redirect or cancellation
+starts no loader work.
+
+### Parallel loader phase
+
+Eligible loaders start concurrently. The first loader error or not-found to
+settle becomes the provisional failure. The choice follows promise settlement
+order, not route order or boundary depth. After settlement, the required render
+prefix is checked root-to-leaf. The first failed ancestor without its own accepted
+`loaderData` property replaces a deeper failure because that deeper boundary is
+not reachable. Locally retained loader data, including an accepted `undefined`,
+keeps the ancestor renderable with stale data and preserves settlement
+chronology. An `undefined` result reconstructed from SSR is intentionally absent
+under the transport policy above.
+
+A redirect is control flow and wins even after an ancestor loader has already
+failed. Reduction therefore waits for started descendant loader work to reveal
+a redirect, including a descendant already refreshing in the
+background. An error or not-found does not cancel already-started descendants
+before this selection completes. Once all relevant work has settled, the first
+such failure is used only if no redirect won. The client retains the full
+structural branch and releases only work that no accepted semantic or background
+candidate owns; the server may abort work below the selected boundary.
+
+Loader settlement does not immediately make each error or not-found terminal.
+As an internal staging state, a failed attempt is reset to `status: 'success'`
+and marked `invalid: true`. This lets already-started descendants settle, allows
+a descendant redirect to remain control flow, and gives reduction one place to
+choose the terminal failure that will be published. Reduction then installs the
+selected error or not-found on its boundary. A failed ancestor without accepted
+loader data replaces a deeper failure because that deeper boundary cannot
+render. Every non-selected failed attempt stays invalid and must reload rather
+than becoming fresh cache data. Semantic `parentMatchPromise` snapshots still
+expose each loader's own outcome to its descendants.
+
+No error-over-not-found sort is performed. The selected not-found is moved to
+its effective not-found boundary; an untargeted not-found searches eligible
+ancestors, while a targeted not-found respects its target.
+
+A global path miss is terminal by `_notFound` even when the selected match
+remains successful and has no error attached. An explicit not-found reduced to
+root also attaches its error there. Both forms cap rendering and hydration and
+produce a 404 response.
+
+For a fuzzy global miss, synchronous matching installs the best boundary visible
+from eager route options. Before contextualization, client and server feed that
+fallback through the same lazy-aware ancestor search used for explicit
+not-found outcomes. This prevents serial hooks below the effective boundary
+from running and prevents loader tasks from starting there, while retaining the
+complete structural branch. The historical deepest-route-with-children fallback
+still applies when no route supplies a not-found component. When `notFoundMode`
+is `'root'`, the search is bypassed but the same execution cap applies at root.
+
+### Chunk readiness
+
+Normal route chunks needed before the selected cutoff are awaited. Although the
+work may start concurrently, readiness outcomes are consumed root-to-leaf. The
+first relevant chunk error replaces a deeper selected serial or loader failure
+because that boundary is no longer reachable; a redirect from relevant
+readiness remains control flow.
+
+The resolved boundary is retained while that selected failure remains current.
+A later lazy retry in the same lane cannot expand the required prefix after its
+readiness has already been consumed.
+Terminal boundary-component preloading is best effort during normal loading; it
+does not start a second failure-selection algorithm.
+
+On the client, lazy/chunk readiness starts independently of loader completion
+and notifies pending selection when it settles. A `pendingComponent` installed
+by lazy options can therefore become the visible boundary while an eager loader
+is still unresolved.
+
+### Projection errors
+
+Client `head` and `scripts`, plus server `head`, `scripts`, and `headers`, are
+decorative with respect to route control flow. They run only after semantic
+reduction. Rejections are logged and swallowed; they never replace the chosen
+loader/before-load outcome or trigger another boundary-selection pass.
+
+The implementation should stay this simple. If a proposed fix requires a new
+error candidate list, ranking pass, boundary score, or convergence loop, it is
+almost certainly rebuilding the discarded complex architecture.
+
+## Terminal commit and lifecycle
+
+A successful client lane remains private through projection. At commit, one
+framework transition publishes semantic matches, cache changes, and route
+lifecycle callbacks.
+
+The client order is:
+
+1. publish final matches/cache and run `onLeave`/`onEnter`/`onStay`,
+2. emit `onLoad`, then `onBeforeRouteMount`, while the transaction is current,
+3. wait for the framework transition acknowledgement to settle,
+4. publish `resolvedLocation` and `idle`,
+5. emit `onResolved`, and
+6. emit `onRendered` only if the acknowledgement was `true` and the same
+ transaction is still current.
+
+Each reentrant callback can start another navigation. Checking that the
+transaction is still current after each publication boundary suppresses stale
+later events. In particular, an `onResolved` navigation suppresses the old
+transaction's `onRendered`.
+
+Route lifecycle callbacks are invoked directly and are expected not to throw.
+The coordinator does not carry a second error-handling path for callback
+failures. All `onLeave` callbacks run before callbacks for retained or newly
+entered routes; the relative ordering of `onEnter` and `onStay` is not a public
+contract.
+
+Server render results are published request-locally and run the documented
+route `onLeave`/`onEnter`/`onStay` callbacks against the previous server
+generation after final matches have been installed. Server redirects do not
+publish a render lane or run those callbacks.
+
+Terminal outcomes do not change route membership. Client and server state keep
+the complete structurally matched branch, lifecycle compares that full branch,
+and renderers derive the visible prefix from match status. Projection likewise
+stops after the terminal match. SSR transport may serialize only that terminal
+prefix because the client can reconstruct hidden structural descendants without
+executing them.
+
+## Pending presentation
+
+Pending UI is presentation, not partial semantic commit.
+
+The first unresolved boundary is the only pending candidate. Its route or the
+router default must provide a pending component, and its effective `pendingMs`
+must allow presentation. Core does not skip an ineligible ancestor to expose an
+unrelated deeper fallback.
+
+A successful route without a loader still participates in chunk readiness and
+projection, but is not changed back to pending merely because a descendant has
+blocking work.
+
+When pending is offered:
+
+- `stores.matches` receives a flight-free snapshot of the complete destination
+ lane;
+- the selected boundary is marked pending;
+- descendants remain observable in state; and
+- the renderer stops at the boundary.
+
+There is one pending session in `router._pending` and one absolute deadline:
+
+```text
+reveal deadline -> exact render acknowledgement -> minimum-visible deadline
+```
+
+The session also remembers the pending component identity. An active client
+lane loading a lazy route chunk can install a more specific `pendingComponent`
+after the default fallback has already rendered. Once that pending component is
+ready, core re-offers the same lane so the framework can replace the fallback
+without creating another pending session or deadline.
+
+The reveal deadline is anchored to the transaction's lane-level `startedAt`.
+Discovering lazy pending options later, advancing to another boundary, or
+retrying the offer does not restart `pendingMs`. If the absolute deadline is
+already past when a pending component becomes eligible, core publishes it in
+the current turn instead of introducing a `setTimeout(0)` race.
+
+`pendingMinMs` starts only after the framework confirms that the pending
+publication rendered. A superseded publication that never rendered creates no
+minimum-visible obligation.
+
+Hydration or a redirect can leave an already visible pending presentation
+without a pending session that owns its original acknowledgement. On takeover,
+core conservatively treats that presentation as rendered and starts its minimum
+from the takeover time instead of delaying its reveal again.
+
+A successor may take over timing only when the boundary index and match ID are
+the same. It keeps the existing deadline but republishes a full snapshot from
+the successor, so pending UI cannot show stale search, params, or context from
+the superseded navigation. Changing the boundary discards the old session.
+
+## Exact framework acknowledgement
+
+`startTransition` returns a promise whose boolean result has a precise meaning:
+
+- settlement means the framework transition can no longer block navigation
+ completion;
+- `true` means the exact requested match publication rendered;
+- `false` means core must finish without emitting `onRendered` or starting a
+ pending minimum based on that publication.
+
+React cannot await `React.startTransition` directly. Its adapter keeps one
+router-owned acknowledgement tuple, and `Matches` settles that tuple from a
+layout effect. A new expected publication first settles the previous receipt as
+`false`, then stores the exact offered array before the transition callback
+runs. This ordering matters because publication can invoke a route lifecycle
+callback that synchronously starts and publishes a successor navigation.
+Installing the expectation after the callback would let the superseded
+publication replace the successor's receipt.
+
+An already-settled router mounted into React uses the same acknowledgement slot
+for its initial `onRendered` event. It therefore emits only after the exact match
+tree and its descendant layout effects have committed, rather than from the
+earlier history-subscription effect.
+
+While a receipt is pending, the aggregate match subscription selects the exact
+offered array instead of reconstructing an equivalent presentation. The layout
+effect acknowledges only that reference. A suspended older render can therefore
+never satisfy a newer receipt merely because its IDs or statuses look the same.
+No generation counter or structural signature is needed.
+
+Solid awaits its transition, and Vue awaits its render tick. For an
+already-settled Solid mount, history subscription remains before the route tree
+while a post-match notifier emits the initial `onRendered` event after
+descendant mount effects.
+
+React assumes one provider and one router. Keeping the tuple on that router lets
+the render tree and core share the exact same receipt without component-local
+generation state, structural signatures, or router-swap machinery.
+
+Every core write passed to `startTransition` must notify the aggregate matches
+store even if much of the lane is structurally reused. Suppressing the write can
+strand the acknowledgement promise.
+
+## `isFetching` and background reloads
+
+`isFetching` is public presentation state. It is observable during normal
+`beforeLoad`, normal loaders, and background loader refreshes.
+
+For a foreground navigation this means the destination match exposes its phase
+once the destination lane is presented. Before pending publication, a
+destination-only match remains private; a same-ID source match can be reconciled
+to the active phase earlier. Background reloads operate on the already
+presented lane and therefore expose their phase immediately.
+
+A background reload keeps successful loader data visible while a private
+candidate runs. The full presented lane remains installed and the affected
+match reports the active phase. Completion clears fetching state whether the
+candidate publishes, fails, or is superseded. A successor may join its loader
+flight, but never adopts the private candidate lane.
+
+Background loader and chunk work may begin and settle while the foreground
+publication renders. Background reduction, projection, and publication do not
+start until the foreground transition acknowledgement settles. A fast refresh
+therefore cannot replace the exact generation that the framework still needs to
+acknowledge.
+
+When background tasks exist, execution creates their settlement observer
+eagerly alongside foreground reduction and retains that promise in the lane
+result. `runBackground` consumes the same settlement chronology after the
+foreground acknowledgement. Recreating the observer then would process work
+that already settled by task attachment or iteration order and could change
+which error, not-found, or descendant redirect wins. Retaining this promise
+preserves the observed settlement order; it does not grant permission to publish
+or define when the lane is complete.
+
+Background work starts from an exact committed base. Before projection it uses a
+fully private lane, including clones of untouched matches, so asynchronous asset
+hooks cannot mutate committed objects without a store publication.
+
+Final background publication requires both:
+
+```text
+router._tx is the owner
+router._committed is the exact base
+```
+
+If either check fails, the entire staged lane is discarded and all candidate
+and clone resources are released. A successful background publication replaces
+the semantic/presented lane atomically but does not change location, foreground
+status, or foreground navigation lifecycle events.
+
+Foreground completion does not join background publication. After the
+foreground acknowledgement, the refresh continues independently and may publish
+before or after `resolvedLocation`, `idle`, and `onResolved`. Active work
+remains publicly observable through `isFetching`; any later publication still
+requires the owner/base identity check.
+
+An error or not-found from background work also stays private through reduction
+and projection, then may atomically replace the successful base with the full
+matched branch carrying its terminal boundary. Hidden descendants retain route
+membership, so background publication does not synthesize leave/enter lifecycle
+events. It is not published incrementally.
+
+Background redirects use the same control-flow and ownership rules as foreground
+redirects. A losing background lane cannot redirect.
+
+## Invalidation, cache clearing, and development refresh
+
+Invalidation creates a new semantic generation and reloads through the normal
+transaction path. It does not turn `stores.matches` into a planning lane.
+
+Filtered invalidation evaluates committed, cached, active-transaction, and
+active-preload matches and collects the selected match IDs. Every committed and
+cached generation with one of those IDs is made invalid. Cached matches are
+already settled successes, so their data owner is marked invalid in place.
+Matching active preload owners are retired first, which prevents older
+speculative work from clearing that stale mark or publishing fresh cache data;
+the lane signal remains the publication fence if its loader outcome already
+fulfilled. The loader discovery entry is also detached before the superseding
+load. Unselected preload lanes remain active. This ID-wide rule prevents a
+cache-first or in-flight same-ID generation from escaping invalidation merely
+because a different generation was the one passed to the filter. Route context
+retains same-ID cacheability across this replacement and needs no separate
+invalidation marker.
+
+Invalidated successful data may remain visible until pending or terminal
+publication, depending on reload mode. Error/not-found generations reset through
+the same loading protocol rather than becoming cache successes.
+
+Cache clearing first snapshots every selected cache match and active preload so
+a throwing public filter changes no authority. It then prunes both authorities
+and directly releases every discarded match lease. When the last lease is
+discarded, cache clearing removes the discovery entry
+instead of preserving the normal zero-owner navigation handoff. Only after all
+leases and discovery entries are detached does it abort the collected flight and
+preload-lane controllers. A public loader signal can synchronously reenter from
+its abort listener; that reentrant load must observe the cleared authorities and
+every removed lease as already detached. Unselected concurrent preloads keep
+shared flights discoverable, and every later cache publication must still have a
+live preload signal and pass the per-match cache-entry identity check captured
+during planning.
+
+Development refresh is deliberately aggressive about reuse. It removes all
+loader flights from discovery, discards active preloads and cache entries, and
+rematches with committed/cache reuse disabled so obsolete params, context,
+loader data, or projected assets cannot seed the refreshed lane. Selected cache
+and preload resources are detached before their controllers are aborted.
+
+Refresh does not immediately discard the accepted committed lane or abort the
+loader signals it still owns. The previous semantic lane, presentation, and
+their resources remain available to the refresh transaction until the new
+publication settles or rolls back. Settlement releases the replaced generation;
+rollback restores it. The ability to roll back belongs to that refresh
+transaction, not to a separate router-global owner.
+
+## Speculative preloading
+
+A preload uses the same match, contextualize, reduce, and project phases as
+navigation, but it never becomes `_tx` and never publishes match presentation.
+
+Its match/cache ownership effects are limited to:
+
+- joinable same-ID loader flights; and
+- individual successful preload loader generations entering the loader cache as
+ they settle.
+
+A preload can also install durable lazy route options through the separate
+route-chunk owner described above. That is route definition readiness, not
+authority over a completed match lane or `beforeLoad` result.
+
+Preload lane-local matching, route context, validation, cancellation, and
+`beforeLoad` outcomes do not become authority for another preload or a later
+navigation. Consumers that join its same-ID loader flight nevertheless share
+that flight's normalized outcome. After a normal standalone preload redirect,
+the private chain continues only if that lane's controller was still present and
+was successfully removed from `_preloads`; replacing an unrelated `_tx` does not
+suppress it. The chain remains depth-bounded, never follows `reloadDocument`,
+and never publishes presentation or history.
+
+The public `preloadRoute` result describes the speculative lane, not merely its
+cacheable subset. An error or not-found therefore resolves with the terminal
+match array while any eligible successful loader generations can still enter
+the cache. Cancellation or control flow that does not yield a reusable lane can
+resolve `undefined`.
+
+Each preload loader task compares the current cache entry for its match ID with
+the entry captured when that task was planned. It cannot overwrite a cache
+generation installed since that plan. Admission happens at loader settlement,
+without waiting for whole-lane success. A distinct successful generation may
+coexist with an older committed same-ID generation; this changes future
+planning precedence, not current presentation. A duplicate sharing the already
+accepted flight is discarded.
+
+`preloadRoute` also works on a server router. It runs the same speculative
+protocol with `preload: true`, can return matches and populate that router's
+loader cache, and does not replace the request's location, committed lane, or
+presentation. Normal request loading still uses the request-local server lane
+and calls its hooks with `preload: false`.
+
+## Server loading and request lifetime
+
+Server loading is request-local, so it does not need the client `_tx`
+coordinator. It still uses the same semantic phases, context ordering, outcome
+normalization, settlement-order failure selection, semantic parent promises,
+and projection behavior.
+
+Each server match gets the public controller passed to its callbacks. The
+request signal—not the mere fact that a callback aborted that controller—is the
+request-liveness authority checked across contextualization, loaders, chunk
+readiness, terminal boundary readiness, and projection. A request abort cancels
+the whole lane. A loader error or not-found does not abort already-started
+descendants before selection, so a later descendant redirect can still win.
+After selection, applying the terminal boundary aborts the hidden suffix that
+the result no longer owns. Redirect aborts the whole request-local lane.
+
+The request signal also governs the surrounding request pipeline. The generic
+handler races manifest lookup, route loading, custom dehydration, and the
+handler/render callback. Start applies the same rule to entry and router
+resolution, middleware, manifest work, and redirect finalization. An abort can
+therefore settle the handler during every awaited phase rather than waiting for
+user code that ignores its signal.
+
+A raced promise may still fulfill after abort. If that late value owns an SSR
+stream, the race disposes it instead of allowing it to regain response or
+cleanup authority. Other user promises may continue executing, but after abort
+they cannot publish a response or inject SSR output. If cleanup occurs while
+application dehydration is awaited, dehydration returns before starting
+serialization; injection and serialization completion also ignore later work.
+
+A server result has one of two forms: render status plus matches, or an HTTP
+redirect. Redirects short-circuit framework rendering and preserve their real
+status, `Location`, and custom headers.
+
+Projection parity is intentional: server `head`, `scripts`, and `headers` use
+the final reduced lane and loader data. As on the client, failures are logged and
+swallowed.
+
+### Stream cleanup handoff
+
+Until a response is accepted, the request handler owns router SSR cleanup. A
+non-stream response, redirect, or failure leaves cleanup with the handler. An
+accepted SSR stream transfers that ownership to the response and is immediately
+bound to the request signal, so an abort after handoff still disposes it.
+
+Disposal is idempotent and severs router SSR ownership before best-effort body
+cancellation. This order matters because a custom or framework stream may ignore
+or indefinitely delay cancellation. Replacing a stream response, resolving a
+redirect from it, or stripping a HEAD body disposes the old stream under the
+same request signal before accepting the replacement.
+
+Framework renderers connect request abort to their upstream renderer as well as
+the router stream transform. Normal completion, downstream cancellation,
+request abort, renderer failure, and stream lifetime timeout all converge on the
+same cleanup authority; none creates a second response owner.
+
+Once a server lane is accepted for rendering, its match controllers are
+registered with that same SSR cleanup authority. They remain live through
+dehydration and response streaming so deferred loader work can finish while the
+response is active, then abort when the response or stream lifetime ends.
+
+## Selective SSR
+
+SSR policy is the first parent-to-child serial step, before route context,
+params/search validation handling, and `beforeLoad`:
+
+- `true` runs server `beforeLoad` and loaders, loads render chunks, projects
+ assets, and renders the component.
+- `'data-only'` runs server `beforeLoad` and loaders and projects `head`,
+ `scripts`, and `headers`, but does not render that route component.
+- `false` skips server `beforeLoad`, loaders, and component chunks. The first
+ `false` boundary still projects `head`, `scripts`, and `headers` for its server
+ shell, then projection stops before descendants that inherit `false`. Route
+ context and params/search validation still run, so their errors remain real
+ server outcomes.
+
+A parent restriction cannot be relaxed by a child: `false` remains false, and a
+`'data-only'` parent caps a child requesting `true` at `'data-only'`.
+
+If a functional `ssr` option throws, the inherited/default policy is established
+before calling it. The failure therefore retains the correct boundary
+renderability instead of leaving `ssr` undefined. An error or not-found from
+the policy still reconstructs route context for its boundary. If route context
+also fails, the original policy failure keeps precedence. Redirects remain
+control flow and skip route-context reconstruction.
+
+Shell mode resolves and dehydrates the root semantic match while the presented
+server lane may include the first client-only pending boundary and its
+descendants. This permits server and initial client presentation to agree.
+
+## Hydration handoff
+
+Hydration reconstructs server work; it does not run a competing hydration
+loader. While reconstruction is asynchronous, its controller is `_preflight`.
+Under the framework-supported startup order, `RouterClient` finishes hydration
+before mounting the provider that starts the initial client load, so no client
+transaction or descendant preload competes with reconstruction. Core does
+not enforce that ordering as a blanket guard. A navigation can supersede hydration
+by installing a new `_preflight`; a preload started directly in this gap is
+unsupported rather than blocked.
+
+The identity of `_preflight` proves that reconstruction is still current. A
+replacement installs itself before aborting the prior controller. The same
+controller interrupts asynchronous application hydration and chunk work, and
+every asynchronous phase checks that identity before mutating or publishing.
+
+Once hydration has accepted a semantic prefix and is ready to publish it,
+`_preflight` is no longer the right authority: no planning operation is in
+progress, but the first normal client load may still need to continue that
+prefix. Hydration therefore installs `_handoff`, a temporary two-phase transfer,
+and detaches its controller from `_preflight`. The handoff is the one owner that
+can decide whether the initial load may continue the prefix; it is not a second
+completion promise or a general cache.
+
+The client router is fresh when hydration begins. Application `hydrate` hooks
+may restore external integration state or update router options, but must not
+call router loading or preloading before reconstruction finishes. Core does not
+guard those calls; this is part of the supported startup order.
+
+The high-level process is:
+
+1. install serialization adapters and application-dehydrated data,
+2. match a fresh candidate lane for the browser location,
+3. accept the identity-compatible serialized lane as the ordered prefix
+ guaranteed by the document protocol,
+4. copy server loader data, `beforeLoad` context, terminal state, and effective
+ SSR policy into private candidates, and install each transported effective
+ SSR value on its route so a functional server policy is not re-evaluated,
+5. start exactly the chunks required by the accepted prefix and any selected
+ terminal boundary concurrently, then consume their outcomes in route order
+ so the earliest failed position can retire its suffix without waiting for
+ irrelevant descendants,
+6. rebuild route context parent-first,
+7. project client `head` and `scripts` through the same projection
+ function used after a normal client load, and
+8. publish accepted semantic work and the complete structural presentation.
+
+Hydration relies on the framework transport contract. For normal and selective
+SSR, the data was produced for the exact document URL by the same route build,
+and its serialized matches form an ordered prefix of the client lane. An SPA
+shell uses the same prefix protocol for its root-only payload; the framework is
+responsible for serving a shell that applies to the document. Core does not
+serialize a second URL identity. Instead, hydration bounds reconstruction to
+the local lane and validates every transported position with a compact match ID.
+
+A mismatch ends the accepted prefix and leaves the local suffix for normal
+client loading. A longer server lane is accepted only through a local global
+not-found boundary that already caps the branch; otherwise no transported
+prefix is accepted. A terminal server error, not-found, or global not-found caps
+client execution so omitted descendants do not run. The client still creates
+those descendants as structurally matched but unexecuted matches. A loaderless
+descendant may initially have `status: 'success'`, so status alone does not prove
+that hydration ran its context or route hooks. Terminal hydration loads the
+chunks required by the selected error/not-found boundary, but not normal
+component chunks below it. Neither route context nor projection runs below the
+transported boundary.
+The transported terminal route remains authoritative even when its effective
+SSR policy is `false` or `'data-only'`: route context or validation was still
+allowed to fail on the server, so hydration must not turn that outcome into an
+unresolved client-only route. Before-load and loader work remain skipped there
+according to the server policy.
+
+Every executed context, `head`, and `scripts` hook nevertheless receives the
+complete locally matched candidate lane. Hook arguments describe structural
+membership; the accepted prefix describes execution authority.
+
+If a required chunk or route-context reconstruction fails, hydration preserves
+only the successfully reconstructed committed prefix. The public presentation
+still contains the complete locally matched lane, so a
+terminal server boundary does not make route membership disappear while its
+client reconstruction is retried. Eligible transported loader successes cross
+the normal loader-cache boundary with merged context and `beforeLoad`
+contribution removed. Because `resolvedLocation` remains unset, normal
+initial client loading retries the failed boundary or context and finishes the
+unresolved suffix.
+
+For a non-terminal selective-SSR handoff, the semantic committed prefix is the
+complete contiguous transported prefix accepted as resolved. The first
+`'data-only'` match is the presentation continuation boundary, but it does not
+truncate semantic adoption: later transported successful data-only matches are
+also committed and keep their server loader and `beforeLoad` results. A `false`,
+pending, or otherwise unresolved match is the first semantic continuation
+boundary and is not committed. Presentation can still contain the complete
+candidate lane and marks its first presentation boundary pending as needed. In
+particular, a shorter non-terminal server payload ending in a pending
+`ssr: false` match is a valid selective-SSR handoff: hydration accepts the
+resolved ancestors, presents the complete local branch, and lets the normal
+initial client load execute from that client-only boundary.
+
+For a successfully reconstructed terminal handoff, transport remains
+prefix-capped but committed and presented membership use the complete locally
+matched branch. Matches below the terminal boundary remain unexecuted and
+hidden; they own no transported `beforeLoad` result or loader data. This keeps
+public membership and lifecycle stable without increasing the SSR payload or
+loading unreachable client chunks. If reconstruction fails, only the accepted
+prefix is committed as described above; the complete branch remains
+presentation, not semantic reuse authority.
+
+Only the subsequent normal client load may transfer the whole hydration
+prefix, and only while the exact committed-prefix owner, raw history-state
+object, browser href, and live hydration controller remain current with no
+active transaction. Core captures the raw browser href and history-state object
+so a reentrant navigation from an initial lifecycle event cannot hand the old
+document's work to a successor location. Finish also validates the transported
+match IDs against the newly matched lane. Core does not coordinate speculative
+work started between raw `hydrate()` and that load; framework adapters own the
+supported ordering.
+
+The two-phase transfer proceeds as follows:
+
+1. Before public navigation events or matching, the initial client-load planner
+ probes the handoff without consuming it.
+2. It installs its own `_preflight`, emits the events, matches a private lane,
+ and asks the handoff to finish the transfer.
+3. Finish revalidates handoff identity, transaction absence, hydration
+ controller liveness, captured location identity, and the exact committed
+ owner.
+4. On rejection, the handoff clears itself before aborting its controller,
+ so abort listeners cannot reenter and claim a rejected handoff.
+5. On acceptance, the accepted matches replace the planner's private copies. A
+ successfully reconstructed terminal handoff owns the complete local branch,
+ even though its SSR payload was prefix-capped, so it has no local suffix to
+ remove. For a non-terminal handoff, the remaining suffix stays in the lane
+ and transfers to the hydration controller so one signal owns the
+ continuation.
+6. The handoff remains available across synchronous reentrancy until the
+ current load installs `_tx`. That load installs `_tx` before clearing the
+ handoff; stale planners may release only their own private work.
+
+This ordering closes the gap between hydration publication and client
+transaction installation without making the handoff an independent completion
+authority. If reconstruction is superseded before publication, the `_preflight`
+identity check aborts its private work. If the published handoff becomes
+incompatible, its failed identity check retires the hydration controller
+and starts normal client loading from a fresh preflight.
+
+Generic framework `RouterClient` components signal streaming hydration
+completion after the hydration attempt settles, including rejection. This
+finally-style handoff allows bootstrap globals to be removed once the server
+stream has also ended without stranding the stream on a hydration failure.
+
+Each framework owns one module-level hydration promise for the document. The
+SSR protocol provides one global bootstrap and one `RouterClient`; additional
+client-only routers use `RouterProvider` and do not independently consume that
+bootstrap. The promise deduplicates framework rendering/replay only. It replaces
+neither `_preflight` reconstruction authority nor `_handoff`
+continuation authority.
+
+## Change checklist
+
+The detailed sections above are the source of truth. Use this shorter checklist
+to find the failure modes a change needs to test; do not duplicate the full
+algorithm here.
+
+### Publication and reentrancy
+
+- Does every asynchronous write recheck its exact owner immediately before the
+ write: `_tx`, `_preflight`, `_handoff`, an active-preload controller, or the
+ request signal?
+- Does a background publication check both its `_tx` and the exact `_committed`
+ base from which it was derived?
+- If an abort, lifecycle callback, or store publication can synchronously
+ reenter, are replacement owners installed and discarded resources detached
+ before that callback can run?
+- Does framework acknowledgement settlement still gate resolved/idle
+ completion, with only an exact rendered publication producing `true` and
+ enabling `onRendered` or `pendingMinMs`?
+
+### Matches, loaders, and cache ownership
+
+- Are `_committed` semantic state and `stores.matches` presentation kept
+ separate? Are match-ID cache compatibility and route-ID lifecycle identity
+ also kept separate?
+- Does every normal client lane run its own `beforeLoad` chain? Hydration is
+ the only completed-work exception; preloads may share loader data or flights,
+ but never their merged context, `beforeLoad` result, or control flow.
+- Is flight discovery separate from lease ownership? Does every copied owner
+ acquire exactly one lease and every discarded owner release it exactly once?
+- Are registry entries and all discarded leases detached before any collected
+ controller is aborted?
+- Do cache admission, invalidation, and clearing preserve the intended
+ generation identity, including the rule that an unexecuted descendant below a
+ cutoff cannot evict a newer same-ID cache entry?
+- Does each child `parentMatchPromise` follow the fresh semantic parent
+ generation rather than the visible stale parent?
+
+### Outcomes and presentation
+
+- Do error/not-found selection, required ancestor readiness, component chunks,
+ and descendant redirects still reduce to one final outcome?
+- Does pending or terminal publication retain the complete structural branch
+ while rendering and outputs apply their relevant cutoff, including the
+ selective-SSR hydration asset-prefix exception?
+- Are pending snapshots flight-free, and do reveal/minimum timing remain owned
+ by one transferable pending session?
+- Do projection failures remain logged and swallowed, and does `isFetching`
+ clear on every success, failure, cancellation, and supersession path?
+
+### Server and hydration lifetime
+
+- Can request abort settle every awaited server phase, and can late work no
+ longer publish a response or injected output?
+- Does exactly one owner perform SSR cleanup, with router ownership released
+ before best-effort stream cancellation?
+- Does hydration validate transported match IDs, execute only the accepted
+ prefix, retain the complete local branch, and preserve any verified
+ `'data-only'` asset prefix?
+- Does the initial-load handoff revalidate its controller, browser history entry,
+ committed owner, and rematched IDs? Do framework adapters still obey the
+ no-preload startup gap described above?
+
+If a fix fails one of these checks, consolidate ownership instead of layering a
+special case on top.
+
+## Testing changes
+
+Tests must assert public behavior, not the internal tuple shape, phase tag,
+private promise, timer, or field name used to implement it.
+
+Useful assertions include:
+
+- rendered pending/error/not-found content and the boundary that rendered it;
+- the complete public matches array and `isFetching` transitions;
+- loader/before-load call counts, contexts, parent promises, and abort signals;
+- navigation completion, lifecycle callbacks, and their surrounding router
+ events, without asserting a relative `onEnter`/`onStay` order;
+- preload/cache reuse observable through user loader calls;
+- HTTP status, headers, redirects, and absence of renderer invocation;
+- request-abort settlement, late-stream disposal, and single stream cleanup;
+- hydration output, completion on rejection, and absence of client reruns below
+ server terminal boundaries;
+- absence of stale data/assets after supersession; and
+- framework acknowledgement tied to the actual rendered destination.
+
+While `tx.done` suspends a pending React destination, React may retain the previous
+source tree in the DOM but hide it. Pending tests should assert visible user
+content, not physical absence of the old route's nodes.
+
+When testing a boundary, give root, parent, and child visibly distinct output.
+An assertion such as `/Not Found/` or `/Error/` can pass at the wrong boundary
+and is not sufficient.
+
+Run focused category tests while changing the loader, then affected core and
+framework unit/type suites, selective-SSR E2E tests for server/hydration changes,
+and the bundle-size benchmark for any client runtime change.
diff --git a/packages/router-core/package.json b/packages/router-core/package.json
index 303e7eca24..a146135251 100644
--- a/packages/router-core/package.json
+++ b/packages/router-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@tanstack/router-core",
- "version": "1.171.14",
+ "version": "1.171.16",
"description": "Modern and scalable routing for React applications",
"author": "Tanner Linsley",
"license": "MIT",
@@ -22,12 +22,12 @@
"clean": "rimraf ./dist && rimraf ./coverage",
"test:eslint": "eslint ./src",
"test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
- "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
"test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js",
"test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js",
"test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js",
"test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js",
- "test:types:ts60": "tsc",
+ "test:types:ts60": "tsc6",
+ "test:types:ts70": "tsc",
"test:build": "publint --strict && attw --ignore-rules no-resolution --pack .",
"test:unit": "vitest",
"test:unit:dev": "pnpm run test:unit --watch",
diff --git a/packages/router-core/skills/router-core/SKILL.md b/packages/router-core/skills/router-core/SKILL.md
index de11b17d28..cadb832037 100644
--- a/packages/router-core/skills/router-core/SKILL.md
+++ b/packages/router-core/skills/router-core/SKILL.md
@@ -5,9 +5,10 @@ description: >-
createRouter, createRoute, createRootRoute, createRootRouteWithContext,
addChildren, Register type declaration, route matching, route sorting,
file naming conventions. Entry point for all router skills.
-type: core
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: core
+ library: tanstack-router
+ library_version: '1.171.15'
---
# TanStack Router Core
@@ -18,6 +19,8 @@ TanStack Router is a type-safe router for React and Solid with built-in SWR cach
> **CRITICAL**: TanStack Router is CLIENT-FIRST. Loaders run on the client by default, NOT server-only like Remix/Next.js. Do not confuse TanStack Router APIs with Next.js or React Router.
+Use this entry skill to choose one primary sub-skill. Do not load the full catalog. Load a second sub-skill only when the task crosses a real boundary, such as an authenticated loader that needs both `auth-and-guards` and `data-loading`.
+
## Sub-Skills
| Task | Sub-Skill |
@@ -66,6 +69,22 @@ Need server-side rendering?
→ router-core/ssr
```
+## Cross-Cutting Completion Checks
+
+For route refactors:
+
+1. Rename or move the route file; do not hand-edit the generated `createFileRoute` path.
+2. Regenerate `routeTree.gen.ts` with the configured Router plugin or CLI.
+3. Update links, redirects, `from` narrowing, params, and tests that reference the old route.
+4. Run type tests and a production build. A typecheck alone does not prove route generation or bundling works.
+
+For response schema changes:
+
+1. Update the source model and shared validation schema.
+2. Update the server function or API serializer so the field exists at runtime.
+3. Update loader and component consumers without casts.
+4. Assert the actual response payload in a unit or integration test. Typechecking cannot catch a serializer that omits the new field.
+
## Minimal Working Example
```tsx
@@ -136,4 +155,4 @@ The plugin auto-generates this string. If you rename a route file, the plugin up
## Version Note
-This skill targets `@tanstack/router-core` v1.166.2 and `@tanstack/react-router` v1.166.2. APIs are stable. Splat routes use `$` (not `*`); the `*` compat alias will be removed in v2.
+This skill targets `@tanstack/router-core` v1.171.15. Splat routes use `$` (not `*`); the `*` compat alias will be removed in v2.
diff --git a/packages/router-core/skills/router-core/auth-and-guards/SKILL.md b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md
index 94d0090d78..71a87a2a3e 100644
--- a/packages/router-core/skills/router-core/auth-and-guards/SKILL.md
+++ b/packages/router-core/skills/router-core/auth-and-guards/SKILL.md
@@ -1,17 +1,17 @@
---
-name: router-core/auth-and-guards
+name: auth-and-guards
description: >-
Route protection with beforeLoad, redirect()/throw redirect(),
isRedirect helper, authenticated layout routes (_authenticated),
non-redirect auth (inline login), RBAC with roles and permissions,
auth provider integration (Auth0, Clerk, Supabase), router context
for auth state.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
- - router-core/data-loading
sources:
- TanStack/router:docs/router/guide/authenticated-routes.md
- TanStack/router:docs/router/how-to/setup-authentication.md
@@ -377,7 +377,7 @@ export const Route = createFileRoute('/_authenticated')({
### CRITICAL: Route guards do not protect server functions
-A `beforeLoad` redirect protects the **route's UI**, not the **server functions** declared on it. `createServerFn` produces an RPC endpoint reachable by direct POST regardless of which route renders the calling UI. An attacker doesn't have to load `/_authenticated/orders` — they can curl the RPC endpoint directly.
+A `beforeLoad` redirect protects the **route's UI**, not the **server functions** declared on it. `createServerFn` produces an RPC endpoint reachable directly with its declared HTTP method regardless of which route renders the calling UI. An attacker doesn't have to load `/_authenticated/orders` — they can call this GET RPC endpoint directly.
```tsx
// WRONG — handler has no auth check; the route guard doesn't help
@@ -410,6 +410,10 @@ const getMyOrders = createServerFn({ method: 'GET' })
Rule of thumb: every `createServerFn`, server route, or API endpoint that touches user data needs `authMiddleware` (or an equivalent in-handler check). The route guard is for the page experience; the endpoint guard is for the data. See [start-core/auth-server-primitives](../../../../start-client-core/skills/start-core/auth-server-primitives/SKILL.md) for the full session/middleware pattern.
+### CRITICAL: The anonymous destination can still disclose protected data
+
+Protect the entire anonymous response, not only the API call. A public login or unauthorized page still leaks data if its title, copy, search params, or serialized loader state names the protected user, tenant, record, or resource. Test a direct anonymous request and follow redirects. Assert that the handler rejects before reading private data, no protected loader runs, the final HTML and serialized state contain no protected identity, and the redirect contains only a sanitized relative return URL.
+
### HIGH: Auth check in component instead of beforeLoad
Component-level auth checks cause a **flash of protected content** before the redirect:
@@ -435,8 +439,6 @@ export const Route = createFileRoute('/_authenticated/dashboard')({
})
```
-`beforeLoad` runs before any component rendering and before the loader. It completely prevents the flash.
-
### HIGH: Not re-throwing redirects in try/catch
`redirect()` works by throwing. If `beforeLoad` has a try/catch, the redirect gets swallowed:
@@ -490,8 +492,6 @@ export const Route = createFileRoute('/_authenticated')({
Place protected routes as children of the `_authenticated` layout route. Public routes (login, home, etc.) live outside it.
----
-
## Cross-References
- See also: **router-core/data-loading/SKILL.md** — `beforeLoad` runs before `loader`; auth context flows into loader via route context
diff --git a/packages/router-core/skills/router-core/code-splitting/SKILL.md b/packages/router-core/skills/router-core/code-splitting/SKILL.md
index 8d1ba4e7a5..aac9098121 100644
--- a/packages/router-core/skills/router-core/code-splitting/SKILL.md
+++ b/packages/router-core/skills/router-core/code-splitting/SKILL.md
@@ -1,13 +1,14 @@
---
-name: router-core/code-splitting
+name: code-splitting
description: >-
Automatic code splitting (autoCodeSplitting), .lazy.tsx convention,
createLazyFileRoute, createLazyRoute, lazyRouteComponent, getRouteApi
for typed hooks in split files, codeSplitGroupings per-route override,
splitBehavior programmatic config, critical vs non-critical properties.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
sources:
diff --git a/packages/router-core/skills/router-core/data-loading/SKILL.md b/packages/router-core/skills/router-core/data-loading/SKILL.md
index 59a71a0cca..6a4364ff1f 100644
--- a/packages/router-core/skills/router-core/data-loading/SKILL.md
+++ b/packages/router-core/skills/router-core/data-loading/SKILL.md
@@ -1,14 +1,15 @@
---
-name: router-core/data-loading
+name: data-loading
description: >-
Route loader option, loaderDeps for cache keys, staleTime/gcTime/
defaultPreloadStaleTime SWR caching, pendingComponent/pendingMs/
pendingMinMs, errorComponent/onError/onCatch, beforeLoad, router
context and createRootRouteWithContext DI pattern, router.invalidate,
Await component, deferred data loading with unawaited promises.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
sources:
@@ -216,13 +217,29 @@ Route-level context via `beforeLoad`:
```tsx
export const Route = createFileRoute('/posts')({
- beforeLoad: () => ({
- fetchPosts: () => fetch('/api/posts').then((r) => r.json()),
+ beforeLoad: ({ context }) => ({
+ fetchPosts: context.fetchPosts,
}),
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})
```
+Keep the implementation SSR-safe when the router is used by TanStack Start. A relative `fetch('/api/posts')` works in a browser event handler, but Node and many server runtimes require an absolute URL during SSR. For app-internal data in Start, call a server function from the loader:
+
+```tsx
+import { createServerFn } from '@tanstack/react-start'
+
+const getPosts = createServerFn({ method: 'GET' }).handler(() => {
+ return db.posts.findMany()
+})
+
+export const Route = createFileRoute('/posts')({
+ loader: () => getPosts(),
+})
+```
+
+Use a server route plus an origin-derived absolute URL only when the HTTP boundary itself is required. Do not hard-code the production origin.
+
### Deferred Data Loading
Return unawaited promises from the loader for non-critical data. Use the `Await` component to render them:
@@ -276,19 +293,17 @@ function AddPostButton() {
const router = useRouter()
const handleAdd = async () => {
- await fetch('/api/posts', { method: 'POST', body: '...' })
- router.invalidate()
+ await createPost({ title: 'New post' })
+ await router.invalidate({ sync: true })
}
return Add Post
}
```
-For synchronous invalidation (wait until loaders finish):
+Use `await router.invalidate({ sync: true })` when the next step requires refreshed loader data.
-```tsx
-await router.invalidate({ sync: true })
-```
+Treat the mutation and invalidation as one workflow. The mutation must persist before invalidation starts, and the loader must read from the same authoritative store. Verify create, update, and delete through the rendered route, including a fresh reload; local component state can hide a stale loader or non-persistent write.
### Error Handling
@@ -361,16 +376,13 @@ export const Route = createFileRoute('/posts')({
},
})
-// CORRECT — loaders run in the browser, use fetch or API calls
+// CORRECT for an SPA — use a client-safe API helper
export const Route = createFileRoute('/posts')({
- loader: async () => {
- const res = await fetch('/api/posts')
- return res.json()
- },
+ loader: () => fetchPosts(),
})
```
-Do NOT put database queries, filesystem access, or server-only code in loaders unless you are using TanStack Start server functions.
+Do NOT put database queries, filesystem access, or server-only code directly in loaders. In TanStack Start, put that work in a server function and call the function from the loader. Do not use a relative `fetch('/api/...')` in an SSR loader.
### MEDIUM: Not understanding staleTime default is 0
diff --git a/packages/router-core/skills/router-core/navigation/SKILL.md b/packages/router-core/skills/router-core/navigation/SKILL.md
index 0e09ddbcec..ba1f0fd2e6 100644
--- a/packages/router-core/skills/router-core/navigation/SKILL.md
+++ b/packages/router-core/skills/router-core/navigation/SKILL.md
@@ -1,14 +1,15 @@
---
-name: router-core/navigation
+name: navigation
description: >-
Link component, useNavigate, Navigate component, router.navigate,
ToOptions/NavigateOptions/LinkOptions, from/to relative navigation,
activeOptions/activeProps, preloading (intent/viewport/render),
preloadDelay, navigation blocking (useBlocker, Block), createLink,
linkOptions helper, scroll restoration, MatchRoute.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
sources:
diff --git a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md
index b4fde048f8..2a69f16888 100644
--- a/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md
+++ b/packages/router-core/skills/router-core/not-found-and-errors/SKILL.md
@@ -1,13 +1,14 @@
---
-name: router-core/not-found-and-errors
+name: not-found-and-errors
description: >-
notFound() function, notFoundComponent, defaultNotFoundComponent,
notFoundMode (fuzzy/root), errorComponent, CatchBoundary,
CatchNotFound, isNotFound, NotFoundRoute (deprecated), route
masking (mask option, createRouteMask, unmaskOnReload).
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
sources:
diff --git a/packages/router-core/skills/router-core/path-params/SKILL.md b/packages/router-core/skills/router-core/path-params/SKILL.md
index 32964d06cc..b4819f04b5 100644
--- a/packages/router-core/skills/router-core/path-params/SKILL.md
+++ b/packages/router-core/skills/router-core/path-params/SKILL.md
@@ -1,13 +1,14 @@
---
-name: router-core/path-params
+name: path-params
description: >-
Dynamic path segments ($paramName), splat routes ($ / _splat),
optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext),
useParams, params.parse/stringify, pathParamsAllowedCharacters,
i18n locale patterns.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
sources:
diff --git a/packages/router-core/skills/router-core/search-params/SKILL.md b/packages/router-core/skills/router-core/search-params/SKILL.md
index 5b509e5a50..0960297d00 100644
--- a/packages/router-core/skills/router-core/search-params/SKILL.md
+++ b/packages/router-core/skills/router-core/search-params/SKILL.md
@@ -1,13 +1,14 @@
---
-name: router-core/search-params
+name: search-params
description: >-
validateSearch, search param validation with Zod/Valibot/ArkType adapters,
fallback(), search middlewares (retainSearchParams, stripSearchParams),
custom serialization (parseSearch, stringifySearch), search param
inheritance, loaderDeps for cache keys, reading and writing search params.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
sources:
diff --git a/packages/router-core/skills/router-core/ssr/SKILL.md b/packages/router-core/skills/router-core/ssr/SKILL.md
index de3fc9a6d7..d1e0524f13 100644
--- a/packages/router-core/skills/router-core/ssr/SKILL.md
+++ b/packages/router-core/skills/router-core/ssr/SKILL.md
@@ -1,5 +1,5 @@
---
-name: router-core/ssr
+name: ssr
description: >-
Non-streaming and streaming SSR, RouterClient/RouterServer,
renderRouterToString/renderRouterToStream, createRequestHandler,
@@ -7,9 +7,10 @@ description: >-
components, head route option (meta/links/styles/scripts),
ScriptOnce, automatic loader dehydration/hydration, memory
history on server, data serialization, document head management.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
- router-core/data-loading
diff --git a/packages/router-core/skills/router-core/type-safety/SKILL.md b/packages/router-core/skills/router-core/type-safety/SKILL.md
index 7b7c53bfa8..ce9204677e 100644
--- a/packages/router-core/skills/router-core/type-safety/SKILL.md
+++ b/packages/router-core/skills/router-core/type-safety/SKILL.md
@@ -1,14 +1,15 @@
---
-name: router-core/type-safety
+name: type-safety
description: >-
Full type inference philosophy (never cast, never annotate inferred
values), Register module declaration, from narrowing on hooks and
Link, strict:false for shared components, getRouteApi for code-split
typed access, addChildren with object syntax for TS perf, LinkProps
and ValidateLinkOptions type utilities, as const satisfies pattern.
-type: sub-skill
-library: tanstack-router
-library_version: '1.166.2'
+metadata:
+ type: sub-skill
+ library: tanstack-router
+ library_version: '1.171.15'
requires:
- router-core
sources:
@@ -489,4 +490,10 @@ const search = Route.useSearch()
If a build error mentions `react-router-dom`, `next/`, `pages/_app`, or duplicate `/` routes, fix the import — don't paper over with type assertions.
+### 6. CRITICAL: Treating typecheck as proof of runtime schema propagation
+
+Types can say a field exists while a database projection, API serializer, or server function omits it. When adding or renaming a field, trace the value through storage, validation, handler output, loader data, and rendered UI. Do not cast the response to the desired type.
+Add a runtime assertion against the real handler or serialized response, such as `expect(await getOrder({ data: { id } })).toMatchObject({ totalCents: 2599 })`.
+Then run the route-level test and production build. The type test remains necessary, but it is not the runtime contract test.
+
See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi).
diff --git a/packages/router-core/src/Matches.ts b/packages/router-core/src/Matches.ts
index 852d186b67..41fd4018b5 100644
--- a/packages/router-core/src/Matches.ts
+++ b/packages/router-core/src/Matches.ts
@@ -9,7 +9,7 @@ import type {
RouteIds,
} from './routeInfo'
import type { AnyRouter, RegisteredRouter, SSROption } from './router'
-import type { Constrain, ControlledPromise } from './utils'
+import type { Constrain } from './utils'
export type AnyMatchAndValue = { match: any; value: any }
@@ -131,47 +131,32 @@ export interface RouteMatch<
pathname: string
params: TAllParams
_strictParams: TAllParams
- status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
+ status: 'pending' | 'success' | 'error' | 'notFound'
isFetching: false | 'beforeLoad' | 'loader'
error: unknown
paramsError: unknown
searchError: unknown
updatedAt: number
- _nonReactive: {
- /** @internal */
- beforeLoadPromise?: ControlledPromise
- /** @internal */
- loaderPromise?: ControlledPromise
- /** @internal */
- pendingTimeout?: ReturnType
- loadPromise?: ControlledPromise
- displayPendingPromise?: Promise
- minPendingPromise?: ControlledPromise
- dehydrated?: boolean
- /** @internal */
- error?: unknown
- }
loaderData?: TLoaderData
+ /** @internal Exclusive end of the SSR-verified asset prefix. */
+ _assetEnd?: number
/** @internal */
- __routeContext?: Record
+ _ctx?: Record
/** @internal */
__beforeLoadContext?: Record
context: TAllContext
search: TFullSearchSchema
_strictSearch: TFullSearchSchema
- fetchCount: number
abortController: AbortController
cause: 'preload' | 'enter' | 'stay'
loaderDeps: TLoaderDeps
preload: boolean
invalid: boolean
headers?: Record
- globalNotFound?: boolean
+ _notFound?: boolean
staticData: StaticDataRouteOption
/** This attribute is not reactive */
ssr?: SSROption
- _forcePending?: boolean
- _displayPending?: boolean
}
export interface PreValidationErrorHandlingRouteMatch<
diff --git a/packages/router-core/src/await-signal.ts b/packages/router-core/src/await-signal.ts
new file mode 100644
index 0000000000..c3506e2abd
--- /dev/null
+++ b/packages/router-core/src/await-signal.ts
@@ -0,0 +1,27 @@
+export function waitForReason(
+ value: T | PromiseLike,
+ signal: AbortSignal,
+ onLate?: (value: T) => void,
+): Promise {
+ const promise = Promise.resolve(value)
+ if (signal.aborted) {
+ if (!onLate) {
+ return Promise.race([Promise.reject(signal.reason), promise])
+ }
+ void promise.then(onLate, () => {})
+ return Promise.reject(signal.reason)
+ }
+ return new Promise((resolve, reject) => {
+ const abort = () => reject(signal.reason)
+ signal.addEventListener('abort', abort, { once: true })
+ promise
+ .then((result) => {
+ if (signal.aborted) {
+ onLate?.(result)
+ } else {
+ resolve(result)
+ }
+ }, reject)
+ .finally(() => signal.removeEventListener('abort', abort))
+ })
+}
diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts
index fd673ca410..a296629ffa 100644
--- a/packages/router-core/src/index.ts
+++ b/packages/router-core/src/index.ts
@@ -103,6 +103,7 @@ export {
resolveManifestCssLink,
} from './manifest'
export { isMatch } from './Matches'
+export { _getAssetMatches, _getRenderedMatches } from './load-client'
export type {
AnyMatchAndValue,
FindValueByIndex,
@@ -242,7 +243,6 @@ export {
SearchParamError,
PathParamError,
getInitialRouterState,
- getMatchedRoutes,
trailingSlashOptions,
} from './router'
@@ -274,9 +274,7 @@ export type {
InjectedHtmlEntry,
EmitFn,
LoadFn,
- GetMatchFn,
SubscribeFn,
- UpdateMatchFn,
CommitLocationFn,
GetMatchRoutesFn,
MatchRoutesFn,
diff --git a/packages/router-core/src/isServer/client.ts b/packages/router-core/src/isServer/client.ts
index f2f86b6ec7..e12407f409 100644
--- a/packages/router-core/src/isServer/client.ts
+++ b/packages/router-core/src/isServer/client.ts
@@ -1 +1,2 @@
export const isServer = false
+export const loadServerRoute: never = undefined as never
diff --git a/packages/router-core/src/isServer/development.ts b/packages/router-core/src/isServer/development.ts
index 3632fabf05..e4f13879ad 100644
--- a/packages/router-core/src/isServer/development.ts
+++ b/packages/router-core/src/isServer/development.ts
@@ -1,2 +1,3 @@
// Development/test mode - returns undefined so fallback to router.isServer is used
export const isServer: boolean | undefined = undefined
+export { loadServerRoute } from '../load-server'
diff --git a/packages/router-core/src/isServer/server.ts b/packages/router-core/src/isServer/server.ts
index cfa181440a..d13401a9e9 100644
--- a/packages/router-core/src/isServer/server.ts
+++ b/packages/router-core/src/isServer/server.ts
@@ -1 +1,2 @@
export const isServer = process.env.NODE_ENV === 'test' ? undefined : true
+export { loadServerRoute } from '../load-server'
diff --git a/packages/router-core/src/link.ts b/packages/router-core/src/link.ts
index 55d5a79ce8..7ec148b8f4 100644
--- a/packages/router-core/src/link.ts
+++ b/packages/router-core/src/link.ts
@@ -671,13 +671,15 @@ export interface LinkOptionsProps {
/**
* The preloading strategy for this link
* - `false` - No preloading
- * - `'intent'` - Preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there.
+ * - `'intent'` - Preload the linked route when the user focuses, hovers over, or touches the link
* - `'viewport'` - Preload the linked route when it enters the viewport
+ * - `'render'` - Preload the linked route as soon as it renders
*/
preload?: false | 'intent' | 'viewport' | 'render'
/**
- * When a preload strategy is set, this delays the preload by this many milliseconds.
- * If the user exits the link before this delay, the preload will be cancelled.
+ * When the intent preload strategy is set, this delays focus and hover
+ * preloading by this many milliseconds. Touch intent preloads immediately.
+ * If focus or hover exits before this delay, the preload will be cancelled.
*/
preloadDelay?: number
/**
diff --git a/packages/router-core/src/load-client.ts b/packages/router-core/src/load-client.ts
new file mode 100644
index 0000000000..83a7697499
--- /dev/null
+++ b/packages/router-core/src/load-client.ts
@@ -0,0 +1,2651 @@
+// Keep this filename free of a secondary extension so declaration generation
+// can rewrite relative imports for both ESM and CJS.
+import { isNotFound } from './not-found'
+import { isRedirect } from './redirect'
+import { getLocationChangeInfo, runRouteLifecycle } from './router'
+import { hydrateSsrMatchId } from './ssr/ssr-match-id'
+import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './ssr/constants'
+import type { AnySerializationAdapter } from './ssr/serializer/transformer'
+import type { TsrSsrGlobal } from './ssr/types'
+import type { ParsedLocation } from './location'
+import type { AnyRouteMatch } from './Matches'
+import type { NotFoundError } from './not-found'
+import type {
+ AnyRoute,
+ BeforeLoadContextOptions,
+ LoaderFnContext,
+ RouteContextOptions,
+ RouteLoaderFn,
+} from './route'
+import type { AnyRedirect } from './redirect'
+import type { AnyRouter } from './router'
+
+type RouteComponentType =
+ | 'component'
+ | 'pendingComponent'
+ | 'errorComponent'
+ | 'notFoundComponent'
+
+export function replaceRouteChunk(
+ route: AnyRoute,
+ lazyFn: AnyRoute['lazyFn'],
+): void {
+ route.lazyFn = lazyFn ?? route.lazyFn
+ route._lazy = undefined
+}
+
+function preloadComponent(
+ route: AnyRoute,
+ type: RouteComponentType,
+): Promise | undefined {
+ return (route.options[type] as any)?.preload?.()
+}
+
+function loadComponents(
+ route: AnyRoute,
+ onPendingReady?: () => void,
+): Promise | undefined {
+ const component = preloadComponent(route, 'component')
+ const pending = preloadComponent(route, 'pendingComponent')
+ const pendingReady =
+ onPendingReady && pending ? pending.then(onPendingReady) : pending
+ if (onPendingReady && !pending) {
+ onPendingReady()
+ }
+ if (component && pendingReady) {
+ return Promise.all([component, pendingReady]).then(() => {})
+ }
+ return component ?? pendingReady
+}
+
+export function loadRouteChunk(
+ route: AnyRoute,
+ // `false` waits only for lazy route options, before a boundary is selected.
+ componentType?: 'errorComponent' | 'notFoundComponent' | false,
+ onPendingReady?: () => void,
+): Promise | undefined {
+ const afterLazy = () =>
+ componentType === false
+ ? undefined
+ : componentType
+ ? preloadComponent(route, componentType)
+ : loadComponents(route, onPendingReady)
+ const current = route._lazy
+ if (current) {
+ return current === true ? afterLazy() : current.then(afterLazy)
+ }
+ if (!route.lazyFn) {
+ return afterLazy()
+ }
+
+ const promise = route.lazyFn().then(
+ (lazyRoute) => {
+ // HMR clears the owner before an obsolete import can settle.
+ if (process.env.NODE_ENV === 'production' || route._lazy === promise) {
+ const { id: _id, ...options } = lazyRoute.options
+ Object.assign(route.options, options)
+ route._lazy = true
+ }
+ },
+ (error) => {
+ if (process.env.NODE_ENV === 'production' || route._lazy === promise) {
+ route._lazy = undefined
+ }
+ throw error
+ },
+ )
+ route._lazy = promise
+ return promise.then(afterLazy)
+}
+
+/** Return the structural lane through the first terminal render boundary. */
+export function _getRenderedMatches(
+ matches: Array,
+): Array {
+ const end =
+ matches.findIndex(
+ (match) => match.status !== 'success' || match._notFound,
+ ) + 1
+ return end && end < matches.length ? matches.slice(0, end) : matches
+}
+
+/** Return the lane whose document assets belong to the current presentation. */
+export function _getAssetMatches(
+ matches: Array,
+): Array {
+ let end = matches.length
+ for (let index = 0; index < end; index++) {
+ const match = matches[index]!
+ // `_assetEnd` is only ever set on hydration presentation clones that are
+ // `status: 'pending'`, `ssr: 'data-only'`, error-free, and not not-found
+ // (see hydrate.ts), and commits clear it — so its presence alone is the guard.
+ if (match._assetEnd !== undefined) {
+ end = Math.min(end, Math.max(index + 1, match._assetEnd))
+ continue
+ }
+ if (match.status !== 'success' || match._notFound) {
+ end = index + 1
+ break
+ }
+ }
+ // `end` only ever shrinks to `index + 1 >= 1`, so no zero guard is needed.
+ return end < matches.length ? matches.slice(0, end) : matches
+}
+
+declare const lanePhase: unique symbol
+
+type LanePhase = 'matched' | 'contextualized' | 'reduced' | 'projected'
+
+/**
+ * Lane matches carry their lane's phase so functions can demand evidence of
+ * pipeline position (e.g. `commitMatches` only accepts a projected lane's
+ * matches). The brand is phantom — it never exists at runtime.
+ */
+type LaneMatches = Array & {
+ readonly [lanePhase]?: TPhase
+}
+
+type Lane = [
+ location: ParsedLocation,
+ matches: LaneMatches