Repository: TanStack/router
Pull request #2525 (2024-10-12 21:00:57)

feat(generator): check if route configuration contains duplicate paths

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit d877771b984aa01c90af6a09913f1410cc2afe8b.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/670ae3a4e0c4f77060314fa2?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/c2011aa1-3f5f-4242-94fa-5b7ee3db4ffd)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/d52a2f30-2eb5-4a4c-a8ce-3d48fb498094)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/8445b304-de60-45ed-bce8-727945cb50cc)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/a9dacc90-1835-4cbf-ad25-50dbe3166640)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/eb077993-4350-4528-af62-592728fceb03)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/eae3f789-c2a4-4d3b-9923-8feeacf6c212)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/3907c7ab-8977-4800-9d6a-4430b4df19d2)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/2f0d356d-7caf-401f-a6af-7d840ce580d5)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/bf9e612b-bf40-48e3-a34f-783638f20201)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/f29522a8-c6bf-4e5f-a14b-a520bc41ad86)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/30368bde-4728-4157-8d20-84d79bb58315)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/d7c5f91e-f5ba-48ea-8acb-cb7831e8f612)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/f9fab76b-f405-4594-b9ec-dfa1eedb27c3)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/84439e4d-2b2e-485b-8ba3-0b5f62db92cf)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/49efcfd6-6415-4f47-a641-5f8f69a4a9c6)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/302013b8-7463-47d6-820e-5aecd34f4f6e)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/8f0ddc78-13c9-492d-88d6-428b6c89b01b)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/f17d8afe-010e-47c2-8299-c333265f0753)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/f43676ad-ab43-4e14-8eb7-989dcb80508e)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/07ffefd8-dcac-4fc0-a36e-19178f08ffcc)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/067b56c9-3c16-493b-89bd-af86ddad19ae)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/5bf11110-e529-4578-b1f0-2084cec4d83d)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/23c7da22-89d9-4f5f-ad11-123cd36e7fce)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/890e54db-cba8-4ba3-b4df-be6910eeb8d0)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/4a034611-64fd-4191-bdf2-3cffd61ad490)
- [tanstack-router-search-validator-adapters](https://pkg.pr.new/template/ccfb6775-e528-4631-9387-1dff2f158300)
- [tanstack-start-example-basic](https://pkg.pr.new/template/08cf75e4-5e7b-4211-bbca-a6da35d9e3b4)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/874a60a1-22c5-4d12-8978-749376a9030e)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/2a008080-6a82-4624-b01b-ee8a7715a065)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/9e0edf21-e6a4-47b6-8a89-782384e80024)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/4845c696-9b1c-4c9d-b85a-5efe1cd16040)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/5a3fc661-5ec6-4194-99b3-63f87e029541)
- [tanstack-start-example-counter](https://pkg.pr.new/template/d114d429-d51c-41a1-a361-09c0bdeab127)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/14351dca-84df-4dba-be06-b0a55d00b546)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/576ad50c-6d5d-463a-8cc7-e8c941a77ff6)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/72af7788-457a-42ce-8122-c6da78d7930e)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/e0840046-852f-4f4d-969d-d45fed2bfeef)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/ca5e1309-8724-4432-9dc3-eb2931f7d103)

</p></details>
      
    


<details><summary><b>@tanstack/eslint-plugin-router</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/eslint-plugin-router@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/create-router</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/create-router@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/history</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/history@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/react-cross-context</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/react-cross-context@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/react-router@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router-with-query</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/react-router-with-query@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-arktype-adapter</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-arktype-adapter@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-cli</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-cli@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-devtools</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-devtools@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-plugin</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-plugin@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-generator</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-generator@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-vite-plugin</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-vite-plugin@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-valibot-adapter</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-valibot-adapter@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/router-zod-adapter</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/router-zod-adapter@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/start</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/start@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/virtual-file-routes</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/virtual-file-routes@2525
```

</p></details>
      
    

<details><summary><b>@tanstack/start-vite-plugin</b></summary><p>

```
pnpm add https://pkg.pr.new/@tanstack/start-vite-plugin@2525
```

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/31451891808"><code>d877771</code></a>_


Patch:
diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts
index f2deeda78..1fbd17943 100644
--- a/packages/router-generator/src/generator.ts
+++ b/packages/router-generator/src/generator.ts
@@ -344,9 +344,16 @@ export const Route = createRootRoute({
   for (const node of preRouteNodes.filter((d) => !d.isAPIRoute)) {
     await handleNode(node)
   }
+  checkRouteFullPathUniqueness(
+    preRouteNodes.filter(
+      (d) => !d.isAPIRoute && d.children === undefined && d.isLazy !== true,
+    ),
+    config,
+  )
 
   const startAPIRouteNodes: Array<RouteNode> = checkStartAPIRoutes(
     preRouteNodes.filter((d) => d.isAPIRoute),
+    config,
   )
 
   const handleAPINode = async (node: RouteNode) => {
@@ -928,7 +935,43 @@ function getFilePathIdAndRouteIdFromPath(pathname?: string) {
   return [filePathId, id] as const
 }
 
-function checkStartAPIRoutes(_routes: Array<RouteNode>) {
+function checkUnique<TElement>(routes: Array<TElement>, key: keyof TElement) {
+  // Check no two routes have the same `key`
+  // if they do, throw an error with the conflicting filePaths
+  const keys = routes.map((d) => d[key])
+  const uniqueKeys = new Set(keys)
+  if (keys.length !== uniqueKeys.size) {
+    const duplicateKeys = keys.filter((d, i) => keys.indexOf(d) !== i)
+    const conflictingFiles = routes.filter((d) =>
+      duplicateKeys.includes(d[key]),
+    )
+    return conflictingFiles
+  }
+  return undefined
+}
+
+function checkRouteFullPathUniqueness(
+  _routes: Array<RouteNode>,
+  config: Config,
+) {
+  const routes = _routes.map((d) => {
+    const inferredFullPath = inferFullPath(d)
+    return { ...d, inferredFullPath }
+  })
+
+  const conflictingFiles = checkUnique(routes, 'inferredFullPath')
+
+  if (conflictingFiles !== undefined) {
+    const errorMessage = `Conflicting configuration paths were found for the following route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles
+      .map((p) => `"${p.inferredFullPath}"`)
+      .join(', ')}.
+Please ensure each route has a unique full path.
+Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n`
+    throw new Error(errorMessage)
+  }
+}
+
+function checkStartAPIRoutes(_routes: Array<RouteNode>, config: Config) {
   if (_routes.length === 0) {
     return []
   }
@@ -942,22 +985,14 @@ function checkStartAPIRoutes(_routes: Array<RouteNode>) {
     return { ...d, routePath }
   })
 
-  // Check no two API routes have the same routePath
-  // if they do, throw an error with the conflicting filePaths
-  const routePaths = routes.map((d) => d.routePath)
-  const uniqueRoutePaths = new Set(routePaths)
-  if (routePaths.length !== uniqueRoutePaths.size) {
-    const duplicateRoutePaths = routePaths.filter(
-      (d, i) => routePaths.indexOf(d) !== i,
-    )
-    const conflictingFiles = routes
-      .filter((d) => duplicateRoutePaths.includes(d.routePath))
-      .map((d) => `${d.fullPath}`)
-    const errorMessage = `Conflicting configuration paths was for found for the following API route${duplicateRoutePaths.length > 1 ? 's' : ''}: ${duplicateRoutePaths
+  const conflictingFiles = checkUnique(routes, 'routePath')
+
+  if (conflictingFiles !== undefined) {
+    const errorMessage = `Conflicting configuration paths were found for the following API route${conflictingFiles.length > 1 ? 's' : ''}: ${conflictingFiles
       .map((p) => `"${p}"`)
       .join(', ')}.
-Please ensure each API route has a unique route path.
-Conflicting files: \n ${conflictingFiles.join('\n ')}\n`
+  Please ensure each API route has a unique route path.
+Conflicting files: \n ${conflictingFiles.map((d) => path.resolve(config.routesDirectory, d.filePath)).join('\n ')}\n`
     throw new Error(errorMessage)
   }
 


Test patch:
diff --git a/packages/router-generator/tests/generator.test.ts b/packages/router-generator/tests/generator.test.ts
index 4ca9b4df0..70fdb8b0b 100644
--- a/packages/router-generator/tests/generator.test.ts
+++ b/packages/router-generator/tests/generator.test.ts
@@ -133,6 +133,13 @@ async function postprocess(folderName: string) {
   }
 }
 
+function shouldThrow(folderName: string) {
+  if (folderName === 'duplicate-fullPath') {
+    return `Conflicting configuration paths were found for the following routes: "/", "/".`
+  }
+  return undefined
+}
+
 describe('generator works', async () => {
   const folderNames = await readDir()
 
@@ -144,17 +151,23 @@ describe('generator works', async () => {
       rewriteConfigByFolderName(folderName, config)
 
       await preprocess(folderName)
-      await generator(config)
-
-      const generatedRouteTree = await getRouteTreeFileText(config)
+      const error = shouldThrow(folderName)
+      if (error) {
+        expect(() => generator(config)).rejects.toThrowError(error)
+      } else {
+        await generator(config)
+
+        const generatedRouteTree = await getRouteTreeFileText(config)
+
+        expect(generatedRouteTree).toMatchFileSnapshot(
+          join(
+            'generator',
+            folderName,
+            `routeTree.snapshot.${config.disableTypes ? 'js' : 'ts'}`,
+          ),
+        )
+      }
 
-      expect(generatedRouteTree).toMatchFileSnapshot(
-        join(
-          'generator',
-          folderName,
-          `routeTree.snapshot.${config.disableTypes ? 'js' : 'ts'}`,
-        ),
-      )
       await postprocess(folderName)
     },
   )
diff --git a/packages/router-generator/tests/generator/duplicate-fullPath/routes/__root.tsx b/packages/router-generator/tests/generator/duplicate-fullPath/routes/__root.tsx
new file mode 100644
index 000000000..f89644e82
--- /dev/null
+++ b/packages/router-generator/tests/generator/duplicate-fullPath/routes/__root.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react'
+import { Outlet, createRootRoute } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+  component: () => (
+    <React.Fragment>
+      <div>Hello "__root"!</div>
+      <Outlet />
+    </React.Fragment>
+  ),
+})
diff --git a/packages/router-generator/tests/generator/duplicate-fullPath/routes/_auth/index.tsx b/packages/router-generator/tests/generator/duplicate-fullPath/routes/_auth/index.tsx
new file mode 100644
index 000000000..56b2ebca7
--- /dev/null
+++ b/packages/router-generator/tests/generator/duplicate-fullPath/routes/_auth/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_auth/')({
+  component: () => <div>Hello /_auth/!</div>,
+})
diff --git a/packages/router-generator/tests/generator/duplicate-fullPath/routes/index.tsx b/packages/router-generator/tests/generator/duplicate-fullPath/routes/index.tsx
new file mode 100644
index 000000000..a680913de
--- /dev/null
+++ b/packages/router-generator/tests/generator/duplicate-fullPath/routes/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+  component: () => <div>Hello /!</div>,
+})