Skip to content

Commit b60eb36

Browse files
fix: fix route mismatch warnings and HMR route indexes (#7422)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 32c5a8e commit b60eb36

24 files changed

Lines changed: 704 additions & 223 deletions

File tree

.changeset/brave-routes-refresh.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/router-core': patch
3+
'@tanstack/router-generator': patch
4+
'@tanstack/router-plugin': patch
5+
---
6+
7+
Fix route mismatch warnings, HMR route index refresh, and generated route type preferences for duplicate pathless/index routes.

packages/router-core/src/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1935,7 +1935,7 @@ export class RouterCore<
19351935
const roundTrip = this.getMatchedRoutes(nextPathname)
19361936
if (roundTrip.foundRoute?.id !== destRoute.id) {
19371937
console.warn(
1938-
`Generated path "${nextPathname}" for route "${destRoute.id}" did not match the same route after params.stringify.`,
1938+
`Generated path "${nextPathname}" for route "${destRoute.id}" matched route "${roundTrip.foundRoute?.id}" instead. This can happen when multiple route templates resolve to the same URL. Use the route template that matches the intended route, or adjust params.stringify if it changed the target path.`,
19391939
)
19401940
}
19411941
} catch {

packages/router-core/tests/build-location.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1265,7 +1265,43 @@ describe('buildLocation - params edge cases', () => {
12651265

12661266
expect(location.pathname).toBe('/no')
12671267
expect(warn).toHaveBeenCalledWith(
1268-
'Generated path "/no" for route "/$foo" did not match the same route after params.stringify.',
1268+
'Generated path "/no" for route "/$foo" matched route "/no" instead. This can happen when multiple route templates resolve to the same URL. Use the route template that matches the intended route, or adjust params.stringify if it changed the target path.',
1269+
)
1270+
} finally {
1271+
warn.mockRestore()
1272+
}
1273+
})
1274+
1275+
test('buildLocation should warn in development when a generated path matches an optional route template', async () => {
1276+
const rootRoute = new BaseRootRoute({})
1277+
const timeRoute = new BaseRoute({
1278+
getParentRoute: () => rootRoute,
1279+
path: '/time',
1280+
})
1281+
const dayRoute = new BaseRoute({
1282+
getParentRoute: () => timeRoute,
1283+
path: '{-$day}',
1284+
})
1285+
1286+
const routeTree = rootRoute.addChildren([timeRoute.addChildren([dayRoute])])
1287+
1288+
const router = createTestRouter({
1289+
routeTree,
1290+
history: createMemoryHistory({ initialEntries: ['/'] }),
1291+
})
1292+
1293+
await router.load()
1294+
1295+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
1296+
1297+
try {
1298+
const location = router.buildLocation({
1299+
to: '/time',
1300+
})
1301+
1302+
expect(location.pathname).toBe('/time')
1303+
expect(warn).toHaveBeenCalledWith(
1304+
'Generated path "/time" for route "/time" matched route "/time/{-$day}" instead. This can happen when multiple route templates resolve to the same URL. Use the route template that matches the intended route, or adjust params.stringify if it changed the target path.',
12691305
)
12701306
} finally {
12711307
warn.mockRestore()

packages/router-generator/src/utils.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,46 @@ const shouldPreferIndexRoute = (
918918
return existing.cleanedPath === '/' && current.cleanedPath !== '/'
919919
}
920920

921+
const isIndexRouteNode = (routeNode: RouteNode): boolean => {
922+
return routeNode.routePath?.endsWith('/') ?? false
923+
}
924+
925+
const isPathlessRouteNode = (routeNode: RouteNode): boolean => {
926+
return routeNode._fsRouteType === 'pathless_layout'
927+
}
928+
929+
const shouldReplaceRouteNodeForTo = (
930+
current: RouteNode,
931+
existing: RouteNode,
932+
): boolean => {
933+
const currentIsIndex = isIndexRouteNode(current)
934+
const existingIsIndex = isIndexRouteNode(existing)
935+
if (currentIsIndex !== existingIsIndex) {
936+
return currentIsIndex
937+
}
938+
939+
const currentIsPathless = isPathlessRouteNode(current)
940+
const existingIsPathless = isPathlessRouteNode(existing)
941+
if (currentIsPathless !== existingIsPathless) {
942+
return !currentIsPathless
943+
}
944+
945+
return true
946+
}
947+
948+
const shouldReplaceRouteNodeForFullPath = (
949+
current: RouteNode,
950+
existing: RouteNode,
951+
): boolean => {
952+
const currentIsPathless = isPathlessRouteNode(current)
953+
const existingIsPathless = isPathlessRouteNode(existing)
954+
if (currentIsPathless !== existingIsPathless) {
955+
return !currentIsPathless
956+
}
957+
958+
return true
959+
}
960+
921961
/**
922962
* Creates a map from fullPath to routeNode
923963
*/
@@ -936,6 +976,11 @@ export const createRouteNodesByFullPath = (
936976
}
937977
}
938978

979+
const existing = map.get(fullPath)
980+
if (existing && !shouldReplaceRouteNodeForFullPath(routeNode, existing)) {
981+
continue
982+
}
983+
939984
map.set(fullPath, routeNode)
940985
}
941986

@@ -953,11 +998,9 @@ export const createRouteNodesByTo = (
953998
for (const routeNode of dedupeBranchesAndIndexRoutes(routeNodes)) {
954999
const to = inferTo(routeNode)
9551000

956-
if (to === '/' && map.has('/')) {
957-
const existing = map.get('/')!
958-
if (shouldPreferIndexRoute(routeNode, existing)) {
959-
continue
960-
}
1001+
const existing = map.get(to)
1002+
if (existing && !shouldReplaceRouteNodeForTo(routeNode, existing)) {
1003+
continue
9611004
}
9621005

9631006
map.set(to, routeNode)

packages/router-generator/tests/generator/nested-layouts/routeTree.snapshot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const JestedLayoutB3LayoutC2BarRoute =
147147

148148
export interface FileRoutesByFullPath {
149149
'/': typeof IndexRoute
150-
'/jested': typeof JestedLayoutB3LayoutC2RouteWithChildren
150+
'/jested': typeof JestedRouteRouteWithChildren
151151
'/foo': typeof LayoutA1FooRoute
152152
'/in-folder': typeof folderInFolderRoute
153153
'/bar': typeof LayoutA2BarRoute
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/* eslint-disable */
2+
3+
// @ts-nocheck
4+
5+
// noinspection JSUnusedGlobalSymbols
6+
7+
// This file was automatically generated by TanStack Router.
8+
// You should NOT make any changes in this file as it will be overwritten.
9+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10+
11+
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as LivestockFarmIdMedicineRouteRouteImport } from './routes/livestock/$farmId/medicine/route'
13+
import { Route as LivestockFarmIdMedicineIndexRouteImport } from './routes/livestock/$farmId/medicine/index'
14+
import { Route as LivestockFarmIdMedicineFormRouteImport } from './routes/livestock/$farmId/medicine/_form'
15+
import { Route as LivestockFarmIdMedicineFormNewRouteImport } from './routes/livestock/$farmId/medicine/_form.new'
16+
17+
const LivestockFarmIdMedicineRouteRoute =
18+
LivestockFarmIdMedicineRouteRouteImport.update({
19+
id: '/livestock/$farmId/medicine',
20+
path: '/livestock/$farmId/medicine',
21+
getParentRoute: () => rootRouteImport,
22+
} as any)
23+
const LivestockFarmIdMedicineIndexRoute =
24+
LivestockFarmIdMedicineIndexRouteImport.update({
25+
id: '/',
26+
path: '/',
27+
getParentRoute: () => LivestockFarmIdMedicineRouteRoute,
28+
} as any)
29+
const LivestockFarmIdMedicineFormRoute =
30+
LivestockFarmIdMedicineFormRouteImport.update({
31+
id: '/_form',
32+
getParentRoute: () => LivestockFarmIdMedicineRouteRoute,
33+
} as any)
34+
const LivestockFarmIdMedicineFormNewRoute =
35+
LivestockFarmIdMedicineFormNewRouteImport.update({
36+
id: '/new',
37+
path: '/new',
38+
getParentRoute: () => LivestockFarmIdMedicineFormRoute,
39+
} as any)
40+
41+
export interface FileRoutesByFullPath {
42+
'/livestock/$farmId/medicine': typeof LivestockFarmIdMedicineRouteRouteWithChildren
43+
'/livestock/$farmId/medicine/': typeof LivestockFarmIdMedicineIndexRoute
44+
'/livestock/$farmId/medicine/new': typeof LivestockFarmIdMedicineFormNewRoute
45+
}
46+
export interface FileRoutesByTo {
47+
'/livestock/$farmId/medicine': typeof LivestockFarmIdMedicineIndexRoute
48+
'/livestock/$farmId/medicine/new': typeof LivestockFarmIdMedicineFormNewRoute
49+
}
50+
export interface FileRoutesById {
51+
__root__: typeof rootRouteImport
52+
'/livestock/$farmId/medicine': typeof LivestockFarmIdMedicineRouteRouteWithChildren
53+
'/livestock/$farmId/medicine/_form': typeof LivestockFarmIdMedicineFormRouteWithChildren
54+
'/livestock/$farmId/medicine/': typeof LivestockFarmIdMedicineIndexRoute
55+
'/livestock/$farmId/medicine/_form/new': typeof LivestockFarmIdMedicineFormNewRoute
56+
}
57+
export interface FileRouteTypes {
58+
fileRoutesByFullPath: FileRoutesByFullPath
59+
fullPaths:
60+
| '/livestock/$farmId/medicine'
61+
| '/livestock/$farmId/medicine/'
62+
| '/livestock/$farmId/medicine/new'
63+
fileRoutesByTo: FileRoutesByTo
64+
to: '/livestock/$farmId/medicine' | '/livestock/$farmId/medicine/new'
65+
id:
66+
| '__root__'
67+
| '/livestock/$farmId/medicine'
68+
| '/livestock/$farmId/medicine/_form'
69+
| '/livestock/$farmId/medicine/'
70+
| '/livestock/$farmId/medicine/_form/new'
71+
fileRoutesById: FileRoutesById
72+
}
73+
export interface RootRouteChildren {
74+
LivestockFarmIdMedicineRouteRoute: typeof LivestockFarmIdMedicineRouteRouteWithChildren
75+
}
76+
77+
declare module '@tanstack/react-router' {
78+
interface FileRoutesByPath {
79+
'/livestock/$farmId/medicine': {
80+
id: '/livestock/$farmId/medicine'
81+
path: '/livestock/$farmId/medicine'
82+
fullPath: '/livestock/$farmId/medicine'
83+
preLoaderRoute: typeof LivestockFarmIdMedicineRouteRouteImport
84+
parentRoute: typeof rootRouteImport
85+
}
86+
'/livestock/$farmId/medicine/': {
87+
id: '/livestock/$farmId/medicine/'
88+
path: '/'
89+
fullPath: '/livestock/$farmId/medicine/'
90+
preLoaderRoute: typeof LivestockFarmIdMedicineIndexRouteImport
91+
parentRoute: typeof LivestockFarmIdMedicineRouteRoute
92+
}
93+
'/livestock/$farmId/medicine/_form': {
94+
id: '/livestock/$farmId/medicine/_form'
95+
path: ''
96+
fullPath: '/livestock/$farmId/medicine'
97+
preLoaderRoute: typeof LivestockFarmIdMedicineFormRouteImport
98+
parentRoute: typeof LivestockFarmIdMedicineRouteRoute
99+
}
100+
'/livestock/$farmId/medicine/_form/new': {
101+
id: '/livestock/$farmId/medicine/_form/new'
102+
path: '/new'
103+
fullPath: '/livestock/$farmId/medicine/new'
104+
preLoaderRoute: typeof LivestockFarmIdMedicineFormNewRouteImport
105+
parentRoute: typeof LivestockFarmIdMedicineFormRoute
106+
}
107+
}
108+
}
109+
110+
interface LivestockFarmIdMedicineFormRouteChildren {
111+
LivestockFarmIdMedicineFormNewRoute: typeof LivestockFarmIdMedicineFormNewRoute
112+
}
113+
114+
const LivestockFarmIdMedicineFormRouteChildren: LivestockFarmIdMedicineFormRouteChildren =
115+
{
116+
LivestockFarmIdMedicineFormNewRoute: LivestockFarmIdMedicineFormNewRoute,
117+
}
118+
119+
const LivestockFarmIdMedicineFormRouteWithChildren =
120+
LivestockFarmIdMedicineFormRoute._addFileChildren(
121+
LivestockFarmIdMedicineFormRouteChildren,
122+
)
123+
124+
interface LivestockFarmIdMedicineRouteRouteChildren {
125+
LivestockFarmIdMedicineFormRoute: typeof LivestockFarmIdMedicineFormRouteWithChildren
126+
LivestockFarmIdMedicineIndexRoute: typeof LivestockFarmIdMedicineIndexRoute
127+
}
128+
129+
const LivestockFarmIdMedicineRouteRouteChildren: LivestockFarmIdMedicineRouteRouteChildren =
130+
{
131+
LivestockFarmIdMedicineFormRoute:
132+
LivestockFarmIdMedicineFormRouteWithChildren,
133+
LivestockFarmIdMedicineIndexRoute: LivestockFarmIdMedicineIndexRoute,
134+
}
135+
136+
const LivestockFarmIdMedicineRouteRouteWithChildren =
137+
LivestockFarmIdMedicineRouteRoute._addFileChildren(
138+
LivestockFarmIdMedicineRouteRouteChildren,
139+
)
140+
141+
const rootRouteChildren: RootRouteChildren = {
142+
LivestockFarmIdMedicineRouteRoute:
143+
LivestockFarmIdMedicineRouteRouteWithChildren,
144+
}
145+
export const routeTree = rootRouteImport
146+
._addFileChildren(rootRouteChildren)
147+
._addFileTypes<FileRouteTypes>()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Outlet, createRootRoute } from '@tanstack/react-router'
2+
3+
export const Route = createRootRoute({
4+
component: () => <Outlet />,
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/livestock/$farmId/medicine/_form/new')({
4+
component: () => <div>New medicine</div>,
5+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Outlet, createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/livestock/$farmId/medicine/_form')({
4+
component: () => <Outlet />,
5+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/livestock/$farmId/medicine/')({
4+
component: () => <div>Medicine index</div>,
5+
})

0 commit comments

Comments
 (0)