Repository: TanStack/router
Pull request #2386 (2024-09-20 21:32:43)

fix(eslint-plugin-router): ignore relative ordering of `params` and `validateSearch`

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

CI is running for commit 4410686fbedee7e704d0ae87fadef001f83f72aa.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/66edea0f21c25459dcf6cbd8?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/405b8979-7e98-4f32-9358-efcc39291ee2)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/ceee303e-47b2-4883-a1b8-57f695b18a15)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/fca746fc-4720-4fef-9bb5-df94ca8c95e7)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/bed19de8-1fb1-4453-b3ad-433604937e25)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/ddb2c1cd-adda-49b2-a13d-593d7fd98f75)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/081bb856-b1a4-4918-b511-5603e9920944)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/265a9036-bbd2-4f6f-b9d6-a4978708d902)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/bf64827e-504c-4645-8b2f-6e43fb0d34da)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/05449ee3-c4b7-40d5-abae-e657996c90ca)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/b7239131-139f-45de-87cf-db0951d3a4f1)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/cea7754c-8a8f-4365-a73c-4cceac1af430)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/320ae33f-deaa-4e54-aa33-2a378be88efb)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/b1705cb9-ca9f-4735-847e-b7f8d7109915)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/76848e49-f16c-410b-8fa2-8567c6707d7c)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/9feae8b3-8c5c-4fd2-b7ea-afd5707f2927)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/e7b3eced-4471-4244-b58e-29cd2054ae13)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/09d0aa13-88a4-492d-be86-1741b4065c9d)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/91c1dea8-2134-44aa-813f-702844848e00)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/1f6fb855-80f6-4a8e-8e60-ea86b04d91ef)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/5e85ff11-75a9-4edb-a06c-66e7a7609e39)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/61ee87e0-5104-4675-a261-f0392b48c230)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/461d95a2-330e-4fed-ae57-ac87d8d768eb)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/273e9a7d-0246-49cd-ad48-85fb448a0b2e)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/27fb5f58-2a53-40af-bc1e-886b117c2961)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/6e1c7ae8-06be-4d8a-8a40-5142a390c95e)
- [tanstack-router-search-validator-adapters](https://pkg.pr.new/template/ae4ff767-202a-4d05-adc4-4c44c2348f48)
- [tanstack-start-example-basic](https://pkg.pr.new/template/a489e18d-3e9b-4039-8e01-74d56d803a22)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/0fd7d809-a013-41ea-a8cd-eeeecc651a7a)
- [tanstack-start-example-basic-counter](https://pkg.pr.new/template/e2576fb3-51cd-441e-ba39-b1f74c3afc9c)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/315e112c-1e66-4182-aced-4afbf7ebd8bf)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/e5839018-3764-481a-abc9-dbec3f6d6c44)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/98765424-4599-4e13-b16e-981c8695fb14)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/08f8de39-c410-462f-87c3-ba3cf8eb9984)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/641db6a0-be54-41f1-b9b5-bab2abc22d78)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/41023b53-447b-461d-bff6-b5813440ade0)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/86789ba5-caff-4785-ba62-e2f064ef8daa)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/3e1c6ab4-66c6-4b1f-a66e-084af146fb9d)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/4b951c86-5056-4314-a61d-c3b93d98ecf0)

</p></details>
      
    


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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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


Patch:
diff --git a/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts b/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
index 9cee80617..1235d10b4 100644
--- a/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
@@ -22,3 +22,10 @@ export const checkedProperties = [
   'loaderDeps',
   'loader',
 ] as const
+
+export const sortRules = [
+  [['params', 'validateSearch'], ['context']],
+  [['context'], ['beforeLoad']],
+  [['beforeLoad'], ['loaderDeps']],
+  [['loaderDeps'], ['loader']],
+] as const
diff --git a/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.rule.ts b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.rule.ts
index 4a33cd939..b8c6be3b8 100644
--- a/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.rule.ts
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.rule.ts
@@ -4,9 +4,9 @@ import { getDocsUrl } from '../../utils/get-docs-url'
 import { detectTanstackRouterImports } from '../../utils/detect-router-imports'
 import { sortDataByOrder } from './create-route-property-order.utils'
 import {
-  checkedProperties,
   createRouteFunctions,
   createRouteFunctionsIndirect,
+  sortRules,
 } from './constants'
 import type { CreateRouteFunction } from './constants'
 import type { ExtraRuleDocs } from '../../types'
@@ -63,8 +63,11 @@ export const rule = createRule({
         }
 
         const allProperties = argument.properties
+        // no need to sort if there is at max 1 property
+        if (allProperties.length < 2) {
+          return
+        }
 
-        // TODO we need to support spread elements, they would be discarded here
         const properties = allProperties.flatMap((p) => {
           if (
             p.type === AST_NODE_TYPES.Property &&
@@ -81,11 +84,7 @@ export const rule = createRule({
           return []
         })
 
-        const sortedProperties = sortDataByOrder(
-          properties,
-          checkedProperties,
-          'name',
-        )
+        const sortedProperties = sortDataByOrder(properties, sortRules, 'name')
         if (sortedProperties === null) {
           return
         }
diff --git a/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts
index 0f7d54a74..390c703e8 100644
--- a/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts
@@ -1,27 +1,56 @@
 export function sortDataByOrder<T, TKey extends keyof T>(
   data: Array<T> | ReadonlyArray<T>,
-  orderArray: Array<T[TKey]> | ReadonlyArray<T[TKey]>,
+  orderRules: ReadonlyArray<
+    Readonly<[ReadonlyArray<T[TKey]>, ReadonlyArray<T[TKey]>]>
+  >,
   key: TKey,
 ): Array<T> | null {
-  const orderMap = new Map(orderArray.map((item, index) => [item, index]))
-
-  // Separate items that are in orderArray from those that are not
-  const inOrderArray = data
-    .filter((item) => orderMap.has(item[key]))
-    .sort((a, b) => {
-      const indexA = orderMap.get(a[key])!
-      const indexB = orderMap.get(b[key])!
+  const getSubsetIndex = (
+    item: T[TKey],
+    subsets: ReadonlyArray<ReadonlyArray<T[TKey]> | Array<T[TKey]>>,
+  ): number | null => {
+    for (let i = 0; i < subsets.length; i++) {
+      if (subsets[i]?.includes(item)) {
+        return i
+      }
+    }
+    return null
+  }
 
-      return indexA - indexB
-    })
+  const orderSets = orderRules.reduce(
+    (sets, [A, B]) => [...sets, A, B],
+    [] as Array<ReadonlyArray<T[TKey]> | Array<T[TKey]>>,
+  )
 
-  const inOrderIterator = inOrderArray.values()
+  const inOrderArray = data.filter(
+    (item) => getSubsetIndex(item[key], orderSets) !== null,
+  )
 
-  // `as boolean` is needed to avoid TS incorrectly inferring that wasResorted is always `true`
   let wasResorted = false as boolean
 
+  // Sort by the relative order defined by the rules
+  const sortedArray = inOrderArray.sort((a, b) => {
+    const aKey = a[key],
+      bKey = b[key]
+    const aSubsetIndex = getSubsetIndex(aKey, orderSets)
+    const bSubsetIndex = getSubsetIndex(bKey, orderSets)
+
+    // If both items belong to different subsets, sort by their subset order
+    if (
+      aSubsetIndex !== null &&
+      bSubsetIndex !== null &&
+      aSubsetIndex !== bSubsetIndex
+    ) {
+      return aSubsetIndex - bSubsetIndex
+    }
+
+    // If both items belong to the same subset or neither is in the subset, keep their relative order
+    return 0
+  })
+
+  const inOrderIterator = sortedArray.values()
   const result = data.map((item) => {
-    if (orderMap.has(item[key])) {
+    if (getSubsetIndex(item[key], orderSets) !== null) {
       const sortedItem = inOrderIterator.next().value!
       if (sortedItem[key] !== item[key]) {
         wasResorted = true


Test patch:
diff --git a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
index 139bd1710..51a0743f4 100644
--- a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
+++ b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
@@ -1,5 +1,6 @@
 import { RuleTester } from '@typescript-eslint/rule-tester'
 import combinate from 'combinate'
+
 import {
   name,
   rule,
@@ -23,6 +24,7 @@ const testedCheckedProperties = [
   checkedProperties[0],
   checkedProperties[1],
   checkedProperties[2],
+  checkedProperties[3],
 ]
 type TestedCheckedProperties = (typeof testedCheckedProperties)[number]
 const orderIndependentProps = ['gcTime', '...foo'] as const
@@ -45,18 +47,49 @@ const validTestMatrix = combinate({
   properties: generatePartialCombinations(testedCheckedProperties, 2),
 })
 
-export function generateInvalidPermutations<T>(
-  arr: ReadonlyArray<T>,
-): Array<{ invalid: Array<T>; valid: Array<T> }> {
+export function generateInvalidPermutations(
+  arr: ReadonlyArray<TestedCheckedProperties>,
+): Array<{
+  invalid: Array<TestedCheckedProperties>
+  valid: Array<TestedCheckedProperties>
+}> {
   const combinations = generatePartialCombinations(arr, 2)
-  const allPermutations: Array<{ invalid: Array<T>; valid: Array<T> }> = []
+  const allPermutations: Array<{
+    invalid: Array<TestedCheckedProperties>
+    valid: Array<TestedCheckedProperties>
+  }> = []
 
   for (const combination of combinations) {
     const permutations = generatePermutations(combination)
     // skip the first permutation as it matches the original combination
     const invalidPermutations = permutations.slice(1)
+
+    if (
+      combination.includes('params') &&
+      combination.includes('validateSearch')
+    ) {
+      if (
+        combination.indexOf('params') < combination.indexOf('validateSearch')
+      ) {
+        // since we ignore the relative order of 'params' and 'validateSearch', we skip this combination (but keep the other one where `validateSearch` is before `params`)
+        continue
+      }
+    }
+
     allPermutations.push(
-      ...invalidPermutations.map((p) => ({ invalid: p, valid: combination })),
+      ...invalidPermutations.map((p) => {
+        // ignore the relative order of 'params' and 'validateSearch'
+        const correctedValid = [...combination].sort((a, b) => {
+          if (
+            (a === 'params' && b === 'validateSearch') ||
+            (a === 'validateSearch' && b === 'params')
+          ) {
+            return p.indexOf(a) - p.indexOf(b)
+          }
+          return checkedProperties.indexOf(a) - checkedProperties.indexOf(b)
+        })
+        return { invalid: p, valid: correctedValid }
+      }),
     )
   }
 
diff --git a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts
index b375ebe3e..b9edd6420 100644
--- a/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts
+++ b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts
@@ -6,43 +6,64 @@ describe('create-route-property-order utils', () => {
     const testCases = [
       {
         data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }],
-        orderArray: ['a', 'b', 'c'],
+        orderArray: [
+          [['a'], ['b']],
+          [['b'], ['c']],
+        ],
         key: 'key',
         expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
       },
       {
         data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }],
-        orderArray: ['a', 'b', 'c'],
+        orderArray: [
+          [['a'], ['b']],
+          [['b'], ['c']],
+        ],
         key: 'key',
         expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
       },
       {
         data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
-        orderArray: ['a', 'b', 'c'],
+        orderArray: [
+          [['a'], ['b']],
+          [['b'], ['c']],
+        ],
         key: 'key',
         expected: null,
       },
       {
         data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }],
-        orderArray: ['a', 'b', 'c'],
+        orderArray: [
+          [['a'], ['b']],
+          [['b'], ['c']],
+        ],
         key: 'key',
         expected: null,
       },
       {
         data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }],
-        orderArray: ['a', 'b', 'c'],
+        orderArray: [
+          [['a'], ['b']],
+          [['b'], ['c']],
+        ],
         key: 'key',
         expected: null,
       },
       {
         data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
-        orderArray: ['a', 'b', 'c'],
+        orderArray: [
+          [['a'], ['b']],
+          [['b'], ['c']],
+        ],
         key: 'key',
         expected: null,
       },
       {
         data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
-        orderArray: ['a', 'b', 'c'],
+        orderArray: [
+          [['a'], ['b']],
+          [['b'], ['c']],
+        ],
         key: 'key',
         expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
       },