Repository: TanStack/router
Pull request #2557 (2024-10-16 18:05:37)

fix(eslint-plugin-router): add missing properties to rule `create-route-property-order`

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

CI is running for commit 295ac327906303b00ff838321faf670745666e81.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/671000908670330afc4ad1a9?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/9bcddd24-d0c8-4641-b38d-f8241d23a95d)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/6aa27129-2292-4df7-8cbf-070afd80a526)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/43eece18-6dba-41c5-87b5-1edcc7df7588)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/7b28190e-64f9-4b42-bc25-807fca83c3ea)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/2f498f4b-b69d-4b2b-99c0-d881bbdaf6a1)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/f6a9dfbf-35ae-4f86-9b8a-d95a7eb0543c)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/7c6ee57b-f93b-443f-869c-d6e053ce273a)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/18fb42c7-03b0-4468-8d56-ec010ce8138e)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/939d7da5-7ad7-4c9e-8b2a-3bb3484ce784)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/955643e6-3b51-4bf9-90f4-546aa888818b)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/4e7b2df1-200d-4f66-a45d-a4240a751c5c)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/210c8abc-2c33-4b5e-a006-1841af605ffb)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/7eb0ac75-213c-4d0e-989c-2137ff41350a)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/89257716-8736-45e0-862f-7304c1f85aa6)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/4be51ea4-9980-4703-a1c2-6facf0a3c6f0)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/c4f48cb8-9f4b-4340-a93c-1251c7dc6b85)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/feb9e0bf-4479-45b3-995b-3f374637bc7d)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/57d8ba2c-7087-4625-be4b-000df55897b8)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/f9c2f49f-63cc-419e-ab46-d5ac1c8dd439)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/95ff74e2-8451-4004-a2c0-48055c9d7fa6)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/8ba84c67-0bd1-4dec-9f8c-84fd5774fc9f)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/f8c51038-0a9b-4f0c-b56d-9653f4cfe444)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/2669a3c6-cba9-4a6d-afd9-62012adcd1f2)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/4a462feb-81f6-4299-8224-8d2aeb193bb1)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/509fb5cd-3295-4219-b40a-bf8c389e8454)
- [tanstack-router-search-validator-adapters](https://pkg.pr.new/template/dc542571-e987-4311-9e76-9f5b75194251)
- [tanstack-start-example-basic](https://pkg.pr.new/template/a59a3910-0319-4756-b667-456e9dfe85c7)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/2eb9b70e-23da-4343-82f1-fd02e0d6a7fb)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/bf9b9d6b-c1ed-4cee-b690-20007f145479)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/743ee9b9-d73e-41f2-ac81-f3912f634953)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/e9888823-a59c-4cea-bbde-af31c3ac14ce)
- [tanstack-start-example-counter](https://pkg.pr.new/template/d0d29e51-d44d-41d8-99d9-450d224440e5)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/1f3fe8b1-7835-412e-9027-196861c51e11)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/7147230a-d9da-484a-949a-0c7b0eb9690a)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/a92dd739-a0c8-432f-92ae-3bb9001f90ec)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/bcc5cf4f-a45f-4a5f-817f-4ff68e962065)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/ddb6f383-6deb-4c02-9414-7ed535e3c25a)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/24bab6ad-204d-4140-b431-d94626ed683d)

</p></details>
      
    


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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/31633168789"><code>295ac32</code></a>_


Patch:
diff --git a/docs/eslint/create-route-property-order.md b/docs/eslint/create-route-property-order.md
index 28d00fb67..56f27821f 100644
--- a/docs/eslint/create-route-property-order.md
+++ b/docs/eslint/create-route-property-order.md
@@ -10,14 +10,14 @@ For the following functions, the property order of the passed in object matters
 - `createRootRoute`
 - `createRootRouteWithContext`
 
-The correct property order is as follows:
+The correct property order is as follows
 
-- `params`
-- `validateSearch`
-- `loaderDeps`
+- `params`, `validateSearch`
+- `loaderDeps`, `search.middlewares`
 - `context`
 - `beforeLoad`
 - `loader`
+- `onEnter`, `onStay`, `onLeave`, `meta`, `links`, `scripts`, `headers`
 
 All other properties are insensitive to the order as they do not depend on type inference.
 
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 74885cd10..1bcd28257 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
@@ -1,3 +1,5 @@
+import { getCheckedProperties } from './create-route-property-order.utils'
+
 export const createRouteFunctionsIndirect = [
   'createFileRoute',
   'createRootRouteWithContext',
@@ -14,18 +16,17 @@ export const createRouteFunctions = [
 
 export type CreateRouteFunction = (typeof createRouteFunctions)[number]
 
-export const checkedProperties = [
-  'params',
-  'validateSearch',
-  'loaderDeps',
-  'context',
-  'beforeLoad',
-  'loader',
-] as const
-
 export const sortRules = [
-  [['params', 'validateSearch'], ['loaderDeps']],
+  [['params', 'validateSearch'], ['search']],
+  [['search'], ['loaderDeps']],
   [['loaderDeps'], ['context']],
   [['context'], ['beforeLoad']],
   [['beforeLoad'], ['loader']],
+  [
+    ['loader'],
+    ['onEnter', 'onStay', 'onLeave', 'meta', 'links', 'scripts', 'headers'],
+  ],
 ] as const
+
+export type CheckedProperties = (typeof sortRules)[number][number][number]
+export const checkedProperties = getCheckedProperties(sortRules)
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 390c703e8..87891e7ab 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
@@ -65,3 +65,9 @@ export function sortDataByOrder<T, TKey extends keyof T>(
   }
   return result
 }
+
+export function getCheckedProperties<T>(
+  orderRules: ReadonlyArray<ReadonlyArray<ReadonlyArray<T>>>,
+): ReadonlyArray<T> {
+  return [...new Set<T>(orderRules.flat(2))]
+}
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index 8736c0244..36dc9ae25 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -142,7 +142,6 @@ export interface RouteMatch<
   paramsError: unknown
   searchError: unknown
   updatedAt: number
-  componentsPromise?: Promise<Array<void>>
   loadPromise?: ControlledPromise<void>
   beforeLoadPromise?: ControlledPromise<void>
   loaderPromise?: ControlledPromise<void>
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index 4129444c2..60ca66c61 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -945,6 +945,7 @@ export class Route<
   rank!: number
   lazyFn?: () => Promise<LazyRoute<any>>
   _lazyPromise?: Promise<void>
+  _componentsPromise?: Promise<Array<void>>
 
   /**
    * @deprecated Use the `createRoute` function instead.
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 986a72c2f..960fe1f84 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -2351,37 +2351,41 @@ export class Router<
 
                         // Actually run the loader and handle the result
                         try {
-                          route._lazyPromise =
-                            route._lazyPromise ||
-                            (route.lazyFn
-                              ? route.lazyFn().then((lazyRoute) => {
-                                  const { id, ...options } = lazyRoute.options
+                          if (route._lazyPromise === undefined) {
+                            if (route.lazyFn) {
+                              route._lazyPromise = route
+                                .lazyFn()
+                                .then((lazyRoute) => {
+                                  // explicitly don't copy over the lazy route's id
+                                  const { id: _id, ...options } =
+                                    lazyRoute.options
                                   Object.assign(route.options, options)
                                 })
-                              : Promise.resolve())
+                            } else {
+                              route._lazyPromise = Promise.resolve()
+                            }
+                          }
 
                           // If for some reason lazy resolves more lazy components...
                           // We'll wait for that before pre attempt to preload any
                           // components themselves.
-                          const componentsPromise =
-                            this.getMatch(matchId)!.componentsPromise ||
-                            route._lazyPromise.then(() =>
-                              Promise.all(
-                                componentTypes.map(async (type) => {
-                                  const component = route.options[type]
-
-                                  if ((component as any)?.preload) {
-                                    await (component as any).preload()
-                                  }
-                                }),
-                              ),
+                          if (route._componentsPromise === undefined) {
+                            route._componentsPromise = route._lazyPromise.then(
+                              () =>
+                                Promise.all(
+                                  componentTypes.map(async (type) => {
+                                    const component = route.options[type]
+                                    if ((component as any)?.preload) {
+                                      await (component as any).preload()
+                                    }
+                                  }),
+                                ),
                             )
+                          }
 
-                          // Otherwise, load the route
                           updateMatch(matchId, (prev) => ({
                             ...prev,
                             isFetching: 'loader',
-                            componentsPromise,
                           }))
 
                           // Kick off the loader!
@@ -2457,9 +2461,9 @@ export class Router<
                           }))
                         }
 
-                        // Last but not least, wait for the the component
+                        // Last but not least, wait for the the components
                         // to be preloaded before we resolve the match
-                        await this.getMatch(matchId)!.componentsPromise
+                        await route._componentsPromise
                       } catch (err) {
                         handleRedirectAndNotFound(this.getMatch(matchId)!, err)
                       }


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 51a0743f4..227530ad2 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
@@ -21,10 +21,10 @@ const ruleTester = new RuleTester()
 
 // reduce the number of test cases by only testing a subset of the checked properties
 const testedCheckedProperties = [
-  checkedProperties[0],
-  checkedProperties[1],
-  checkedProperties[2],
-  checkedProperties[3],
+  checkedProperties[0]!,
+  checkedProperties[1]!,
+  checkedProperties[2]!,
+  checkedProperties[3]!,
 ]
 type TestedCheckedProperties = (typeof testedCheckedProperties)[number]
 const orderIndependentProps = ['gcTime', '...foo'] as const
@@ -165,6 +165,19 @@ const validTestCases = validTestMatrix.map(
   }),
 )
 
+invalidTestMatrix.push({
+  createRouteFunction: 'createFileRoute',
+  properties: {
+    invalid: ['loader', 'loaderDeps'],
+    valid: ['loaderDeps', 'loader'],
+  },
+})
+
+invalidTestMatrix.push({
+  createRouteFunction: 'createFileRoute',
+  properties: { invalid: ['meta', 'loader'], valid: ['loader', 'meta'] },
+})
+
 const invalidTestCases = invalidTestMatrix.map(
   ({ createRouteFunction, properties }) => ({
     name: `incorrect property order is detected for ${createRouteFunction} with order: ${properties.invalid.join(', ')}`,
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 b9edd6420..4dfb782e6 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
@@ -1,5 +1,8 @@
 import { describe, expect, test } from 'vitest'
-import { sortDataByOrder } from '../rules/create-route-property-order/create-route-property-order.utils'
+import {
+  getCheckedProperties,
+  sortDataByOrder,
+} from '../rules/create-route-property-order/create-route-property-order.utils'
 
 describe('create-route-property-order utils', () => {
   describe('sortDataByOrder', () => {
@@ -67,6 +70,63 @@ describe('create-route-property-order utils', () => {
         key: 'key',
         expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
       },
+      {
+        data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
+        orderArray: [
+          [['a', 'b'], ['d']],
+          [['d'], ['c']],
+        ],
+        key: 'key',
+        expected: [{ key: 'b' }, { key: 'a' }, { key: 'd' }, { key: 'c' }],
+      },
+      {
+        data: [
+          { key: 'd' },
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'c' },
+          { key: 'f' },
+        ],
+        orderArray: [
+          [
+            ['a', 'b'],
+            ['d', 'f'],
+          ],
+          [['d'], ['c']],
+        ],
+        key: 'key',
+        expected: [
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'd' },
+          { key: 'f' },
+          { key: 'c' },
+        ],
+      },
+      {
+        data: [
+          { key: 'd' },
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'c' },
+          { key: 'f' },
+        ],
+        orderArray: [
+          [
+            ['a', 'b'],
+            ['d', 'f'],
+          ],
+          [['d', 'f'], ['c']],
+        ],
+        key: 'key',
+        expected: [
+          { key: 'b' },
+          { key: 'a' },
+          { key: 'd' },
+          { key: 'f' },
+          { key: 'c' },
+        ],
+      },
     ] as const
     test.each(testCases)(
       '$data $orderArray $key $expected',
@@ -77,3 +137,43 @@ describe('create-route-property-order utils', () => {
     )
   })
 })
+
+describe('getCheckedProperties', () => {
+  const testCases = [
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['c'], ['d']],
+      ],
+      expected: ['a', 'b', 'c', 'd'],
+    },
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['d'], ['e']],
+      ],
+      expected: ['a', 'b', 'c', 'd', 'e'],
+    },
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['d'], ['e']],
+        [['c'], ['f']],
+      ],
+      expected: ['a', 'b', 'c', 'd', 'e', 'f'],
+    },
+    {
+      orderRules: [
+        [['a', 'b'], ['c']],
+        [['d'], ['e']],
+        [['c'], ['f']],
+        [['f'], ['g']],
+      ],
+      expected: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
+    },
+  ] as const
+  test.each(testCases)('$orderRules $expected', ({ orderRules, expected }) => {
+    const checkedProperties = getCheckedProperties(orderRules)
+    expect(checkedProperties).toEqual(expected)
+  })
+})