Repository: TanStack/router
Pull request #2365 (2024-09-18 17:41:34)

fix(react-router): correctly preload split routes without loaders 

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

CI is running for commit 4f4908c7a35d6f8e865bc31d7b3c7e893caee8e9.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/66eb10e8307598178c372d80?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/b03d5d60-8698-486f-b329-ef130ad4339a)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/197b6596-4b1f-4c06-b50f-4801ab19d510)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/8b59d11f-23be-4ccb-8587-5d33516f8613)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/89e721c2-ccda-4f14-a6f9-2445c9147b36)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/5791d489-3ceb-4737-a330-f35dc47d7206)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/b0b88de4-5539-4fb7-9986-bebd23e00858)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/216a6490-ba28-4ba6-870b-4e1c0777a005)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/30330c73-5c6b-42f6-a06b-243cbeebd659)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/30d91646-5026-48e2-8724-8c707bb2663d)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/d6278c9d-7e25-47f1-856e-3632ff8de7a3)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/d073b989-514a-4873-9506-58faaaa4e6a1)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/bfde6262-bc8c-4622-a670-a5e9d031a1da)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/d183e7c8-a1d6-4f51-a58a-8065315434d7)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/17113192-6952-466b-939d-05b88ca0e437)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/7d4893b2-6b7c-4a7b-a5c5-e6abb3b77b50)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/4ab936f8-a09c-4d7f-81d2-5b04d16e8485)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/e4728a8f-d65d-4c8c-90d4-0f395f971cfe)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/76a88c53-ab6c-46d4-9c72-73287fd2779f)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/0c0c2733-41d0-4496-b753-fa80710bba50)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/8d1850db-3631-40c9-bdff-9b2329a5fbba)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/3ed54246-b68a-4d37-9860-751938002b42)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/4580c671-fbab-4ba8-939a-57079afd62c7)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/fa8b7b07-652f-45e0-9ac6-386d79a7babe)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/debae583-0c81-4737-89d8-9cb1c37be367)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/06b8995d-eb2d-412c-9a81-635358b80f29)
- [tanstack-router-search-validator-adapters](https://pkg.pr.new/template/c5d49f48-4c06-4ccd-b784-9e04590a0ad0)
- [tanstack-start-example-basic](https://pkg.pr.new/template/e0548242-d26e-443a-bde3-c97e6eab5e4c)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/3c9e363a-b17d-40cd-8556-b400b2f428af)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/b621b374-c8ff-4395-af49-1ee6d95c5b71)
- [tanstack-start-example-basic-counter](https://pkg.pr.new/template/60ecc4ca-a9ce-465a-a455-7b02be3e24ae)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/5da6f48c-ac56-4849-ade8-55f3bd7dfb44)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/0fa34b6a-8b82-4d0b-8a0e-92e6e6d04d0a)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/b2e8ec38-b870-4d4e-8df9-ef9efc74021a)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/53cf1454-ad60-4779-ad9e-5711e74afdbe)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/72630f1f-6ee3-4e68-a6bd-dd5e34597e26)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/bc5be15e-42b8-4d79-be0a-03c19c4f904e)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/46a41bec-6f83-4cbc-97c9-d28c9455bd45)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/6b64f651-0b17-47a8-9528-a3bb6196697a)

</p></details>
      
    


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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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


Patch:
diff --git a/docs/config.json b/docs/config.json
index 2ba8d8f0f..948508f6d 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -238,6 +238,19 @@
         }
       ]
     },
+    {
+      "label": "ESLint",
+      "children": [
+        {
+          "label": "ESLint Plugin Router",
+          "to": "eslint/eslint-plugin-router"
+        },
+        {
+          "label": "Create Route Property Order",
+          "to": "eslint/create-route-property-order"
+        }
+      ]
+    },
     {
       "label": "Router Examples",
       "children": [],
diff --git a/docs/eslint/create-route-property-order.md b/docs/eslint/create-route-property-order.md
new file mode 100644
index 000000000..fa82f1dc5
--- /dev/null
+++ b/docs/eslint/create-route-property-order.md
@@ -0,0 +1,57 @@
+---
+id: create-route-property-order
+title: Ensure correct order of inference sensitive properties for createRoute functions
+---
+
+For the following functions, the property order of the passed in object matters due to type inference:
+
+- `createRoute`
+- `createFileRoute`
+- `createRootRoute`
+- `createRootRouteWithContext`
+
+The correct property order is as follows:
+
+- `params`
+- `validateSearch`
+- `context`
+- `beforeLoad`
+- `loaderDeps`
+- `loader`
+
+All other properties are insensitive to the order as they do not depend on type inference.
+
+## Rule Details
+
+Examples of **incorrect** code for this rule:
+
+```tsx
+/* eslint "@tanstack/router/create-route-property-order": "warn" */
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/bar/$id')({
+  loader: async ({context}) => {
+    await context.queryClient.ensureQueryData(getQueryOptions(context.hello)),
+  },
+  beforeLoad: () => ({hello: 'world'})
+})
+```
+
+Examples of **correct** code for this rule:
+
+```tsx
+/* eslint "@tanstack/router/create-route-property-order": "warn" */
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/foo/bar/$id')({
+  beforeLoad: () => ({hello: 'world'}),
+  loader: async ({context}) => {
+    await context.queryClient.ensureQueryData(getQueryOptions(context.hello)),
+  }
+})
+```
+
+## Attributes
+
+- [x] ✅ Recommended
+- [x] 🔧 Fixable
diff --git a/docs/eslint/eslint-plugin-router.md b/docs/eslint/eslint-plugin-router.md
new file mode 100644
index 000000000..ccfd7d157
--- /dev/null
+++ b/docs/eslint/eslint-plugin-router.md
@@ -0,0 +1,96 @@
+---
+id: eslint-plugin-router
+title: ESLint Plugin Router
+---
+
+TanStack Router comes with its own ESLint plugin. This plugin is used to enforce best practices and to help you avoid common mistakes.
+
+## Installation
+
+The plugin is a separate package that you need to install:
+
+```bash
+$ npm i -D @tanstack/eslint-plugin-router
+```
+
+or
+
+```bash
+$ pnpm add -D @tanstack/eslint-plugin-router
+```
+
+or
+
+```bash
+$ yarn add -D @tanstack/eslint-plugin-router
+```
+
+or
+
+```bash
+$ bun add -D @tanstack/eslint-plugin-router
+```
+
+## Flat Config (`eslint.config.js`)
+
+### Recommended setup
+
+To enable all of the recommended rules for our plugin, add the following config:
+
+```js
+import pluginRouter from '@tanstack/eslint-plugin-router'
+
+export default [
+  ...pluginRouter.configs['flat/recommended'],
+  // Any other config...
+]
+```
+
+### Custom setup
+
+Alternatively, you can load the plugin and configure only the rules you want to use:
+
+```js
+import pluginRouter from '@tanstack/eslint-plugin-router'
+
+export default [
+  {
+    plugins: {
+      '@tanstack/router': pluginRouter,
+    },
+    rules: {
+      '@tanstack/router/create-route-property-order': 'error',
+    },
+  },
+  // Any other config...
+]
+```
+
+## Legacy Config (`.eslintrc`)
+
+### Recommended setup
+
+To enable all of the recommended rules for our plugin, add `plugin:@tanstack/eslint-plugin-router/recommended` in extends:
+
+```json
+{
+  "extends": ["plugin:@tanstack/eslint-plugin-router/recommended"]
+}
+```
+
+### Custom setup
+
+Alternatively, add `@tanstack/eslint-plugin-router` to the plugins section, and configure the rules you want to use:
+
+```json
+{
+  "plugins": ["@tanstack/eslint-plugin-router"],
+  "rules": {
+    "@tanstack/router/create-route-property-order": "error"
+  }
+}
+```
+
+## Rules
+
+- [@tanstack/router/create-route-property-order](../create-route-property-order)
diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json
index c75703559..b1b71e9c7 100644
--- a/examples/react/authenticated-routes/package.json
+++ b/examples/react/authenticated-routes/package.json
@@ -12,7 +12,6 @@
     "@tanstack/react-router": "^1.57.18",
     "@tanstack/router-devtools": "^1.57.18",
     "@tanstack/router-plugin": "^1.57.19",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json
index cc678bbb5..a74726763 100644
--- a/examples/react/basic-file-based-codesplitting/package.json
+++ b/examples/react/basic-file-based-codesplitting/package.json
@@ -12,7 +12,6 @@
     "@tanstack/react-router": "^1.57.18",
     "@tanstack/router-devtools": "^1.57.18",
     "@tanstack/router-plugin": "^1.57.19",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json
index b883393af..a49b51328 100644
--- a/examples/react/basic-file-based/package.json
+++ b/examples/react/basic-file-based/package.json
@@ -12,7 +12,6 @@
     "@tanstack/react-router": "^1.57.18",
     "@tanstack/router-devtools": "^1.57.18",
     "@tanstack/router-plugin": "^1.57.19",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json
index 6837bed84..c97f3d382 100644
--- a/examples/react/deferred-data/package.json
+++ b/examples/react/deferred-data/package.json
@@ -12,7 +12,6 @@
     "@tanstack/react-router": "^1.57.18",
     "@tanstack/router-devtools": "^1.57.18",
     "redaxios": "^0.5.1",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "zod": "^3.23.8"
diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json
index 30e23f63c..557361f1b 100644
--- a/examples/react/large-file-based/package.json
+++ b/examples/react/large-file-based/package.json
@@ -15,7 +15,6 @@
     "@tanstack/react-router": "^1.57.18",
     "@tanstack/router-devtools": "^1.57.18",
     "@tanstack/router-plugin": "^1.57.19",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json
index 3382afd3b..56ea4384d 100644
--- a/examples/react/quickstart-file-based/package.json
+++ b/examples/react/quickstart-file-based/package.json
@@ -12,7 +12,6 @@
     "@tanstack/react-router": "^1.57.18",
     "@tanstack/router-devtools": "^1.57.18",
     "@tanstack/router-plugin": "^1.57.19",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json
index 2d2ea4df5..103cb0d7d 100644
--- a/examples/react/with-framer-motion/package.json
+++ b/examples/react/with-framer-motion/package.json
@@ -13,7 +13,6 @@
     "@tanstack/router-devtools": "^1.57.18",
     "redaxios": "^0.5.1",
     "framer-motion": "^11.5.4",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "zod": "^3.23.8"
diff --git a/package.json b/package.json
index 7d88a2f78..034e85df3 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,8 @@
     "test:build": "nx affected --target=test:build --exclude=examples/**",
     "test:types": "nx affected --target=test:types --exclude=examples/**",
     "test:e2e": "nx run-many --target=test:e2e",
-    "build": "nx affected --target=build --exclude=examples/**",
-    "build:all": "nx run-many --target=build --exclude=examples/**",
+    "build": "nx affected --target=build --exclude=e2e/** --exclude=examples/**",
+    "build:all": "nx run-many --target=build --exclude=examples/** --exclude=e2e/** ",
     "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all",
     "dev": "pnpm run watch",
     "prettier": "prettier --ignore-unknown '**/*'",
@@ -79,6 +79,7 @@
       "@tanstack/router-arktype-adapter": "workspace:*",
       "@tanstack/start": "workspace:*",
       "@tanstack/start-vite-plugin": "workspace:*",
+      "@tanstack/eslint-plugin-router": "workspace:*",
       "temp-react": "0.0.0-experimental-035a41c4e-20230704",
       "temp-react-dom": "0.0.0-experimental-035a41c4e-20230704"
     }
diff --git a/packages/eslint-plugin-router/.attw.json b/packages/eslint-plugin-router/.attw.json
new file mode 100644
index 000000000..329bfe834
--- /dev/null
+++ b/packages/eslint-plugin-router/.attw.json
@@ -0,0 +1,3 @@
+{
+  "ignoreRules": ["false-export-default"]
+}
diff --git a/packages/eslint-plugin-router/eslint.config.js b/packages/eslint-plugin-router/eslint.config.js
new file mode 100644
index 000000000..8ce6ad05f
--- /dev/null
+++ b/packages/eslint-plugin-router/eslint.config.js
@@ -0,0 +1,5 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+export default [...rootConfig]
diff --git a/packages/eslint-plugin-router/package.json b/packages/eslint-plugin-router/package.json
new file mode 100644
index 000000000..7e0cbdd51
--- /dev/null
+++ b/packages/eslint-plugin-router/package.json
@@ -0,0 +1,65 @@
+{
+  "name": "@tanstack/eslint-plugin-router",
+  "version": "1.58.0",
+  "description": "ESLint plugin for TanStack Router",
+  "author": "Manuel Schiller",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/TanStack/router.git",
+    "directory": "packages/eslint-plugin-router"
+  },
+  "homepage": "https://tanstack.com/router",
+  "funding": {
+    "type": "github",
+    "url": "https://github.com/sponsors/tannerlinsley"
+  },
+  "scripts": {
+    "clean": "rimraf ./dist ./coverage",
+    "test:eslint": "eslint ./src",
+    "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+    "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js",
+    "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
+    "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js",
+    "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
+    "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js",
+    "test:types:ts56": "tsc",
+    "test:unit": "vitest",
+    "test:unit:dev": "pnpm run test:unit --watch --typecheck",
+    "test:build": "publint --strict && attw --pack .",
+    "build": "vite build"
+  },
+  "type": "module",
+  "types": "dist/esm/index.d.ts",
+  "main": "dist/cjs/index.cjs",
+  "module": "dist/esm/index.js",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/esm/index.d.ts",
+        "default": "./dist/esm/index.js"
+      },
+      "require": {
+        "types": "./dist/cjs/index.d.cts",
+        "default": "./dist/cjs/index.cjs"
+      }
+    },
+    "./package.json": "./package.json"
+  },
+  "sideEffects": false,
+  "files": [
+    "dist",
+    "src"
+  ],
+  "dependencies": {
+    "@typescript-eslint/utils": "^8.3.0"
+  },
+  "devDependencies": {
+    "@typescript-eslint/rule-tester": "^8.3.0",
+    "combinate": "^1.1.11",
+    "eslint": "^9.9.1"
+  },
+  "peerDependencies": {
+    "eslint": "^8.57.0 || ^9.0.0"
+  }
+}
diff --git a/packages/eslint-plugin-router/src/index.ts b/packages/eslint-plugin-router/src/index.ts
new file mode 100644
index 000000000..f02ea5a00
--- /dev/null
+++ b/packages/eslint-plugin-router/src/index.ts
@@ -0,0 +1,43 @@
+import { rules } from './rules'
+import type { ESLint, Linter } from 'eslint'
+import type { RuleModule } from '@typescript-eslint/utils/ts-eslint'
+
+type RuleKey = keyof typeof rules
+
+interface Plugin extends Omit<ESLint.Plugin, 'rules'> {
+  rules: Record<RuleKey, RuleModule<any, any, any>>
+  configs: {
+    recommended: ESLint.ConfigData
+    'flat/recommended': Array<Linter.FlatConfig>
+  }
+}
+
+const plugin: Plugin = {
+  meta: {
+    name: '@tanstack/eslint-plugin-router',
+  },
+  configs: {} as Plugin['configs'],
+  rules,
+}
+
+// Assign configs here so we can reference `plugin`
+Object.assign(plugin.configs, {
+  recommended: {
+    plugins: ['@tanstack/eslint-plugin-router'],
+    rules: {
+      '@tanstack/router/create-route-property-order': 'warn',
+    },
+  },
+  'flat/recommended': [
+    {
+      plugins: {
+        '@tanstack/router': plugin,
+      },
+      rules: {
+        '@tanstack/router/create-route-property-order': 'warn',
+      },
+    },
+  ],
+})
+
+export default plugin
diff --git a/packages/eslint-plugin-router/src/rules.ts b/packages/eslint-plugin-router/src/rules.ts
new file mode 100644
index 000000000..73219d401
--- /dev/null
+++ b/packages/eslint-plugin-router/src/rules.ts
@@ -0,0 +1,15 @@
+import * as createRoutePropertyOrder from './rules/create-route-property-order/create-route-property-order.rule'
+import type { ESLintUtils } from '@typescript-eslint/utils'
+import type { ExtraRuleDocs } from './types'
+
+export const rules: Record<
+  string,
+  ESLintUtils.RuleModule<
+    string,
+    ReadonlyArray<unknown>,
+    ExtraRuleDocs,
+    ESLintUtils.RuleListener
+  >
+> = {
+  [createRoutePropertyOrder.name]: createRoutePropertyOrder.rule,
+}
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
new file mode 100644
index 000000000..9cee80617
--- /dev/null
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/constants.ts
@@ -0,0 +1,24 @@
+export const createRouteFunctionsIndirect = [
+  'createFileRoute',
+  'createRootRouteWithContext',
+] as const
+export const createRouteFunctionsDirect = [
+  'createRootRoute',
+  'createRoute',
+] as const
+
+export const createRouteFunctions = [
+  ...createRouteFunctionsDirect,
+  ...createRouteFunctionsIndirect,
+] as const
+
+export type CreateRouteFunction = (typeof createRouteFunctions)[number]
+
+export const checkedProperties = [
+  'params',
+  'validateSearch',
+  'context',
+  'beforeLoad',
+  '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
new file mode 100644
index 000000000..4a33cd939
--- /dev/null
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.rule.ts
@@ -0,0 +1,125 @@
+import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'
+
+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,
+} from './constants'
+import type { CreateRouteFunction } from './constants'
+import type { ExtraRuleDocs } from '../../types'
+
+const createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)
+
+const createRouteFunctionSet = new Set(createRouteFunctions)
+function isCreateRouteFunction(node: any): node is CreateRouteFunction {
+  return createRouteFunctionSet.has(node)
+}
+
+export const name = 'create-route-property-order'
+
+export const rule = createRule({
+  name,
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'Ensure correct order of inference sensitive properties for createRoute functions',
+      recommended: 'error',
+    },
+    messages: {
+      invalidOrder: 'Invalid order of properties for `{{function}}`.',
+    },
+    schema: [],
+    hasSuggestions: true,
+    fixable: 'code',
+  },
+  defaultOptions: [],
+
+  create: detectTanstackRouterImports((context) => {
+    return {
+      CallExpression(node) {
+        if (node.callee.type !== AST_NODE_TYPES.Identifier) {
+          return
+        }
+        const createRouteFunction = node.callee.name
+        if (!isCreateRouteFunction(createRouteFunction)) {
+          return
+        }
+        let args = node.arguments
+        if (createRouteFunctionsIndirect.includes(createRouteFunction as any)) {
+          if (node.parent.type === AST_NODE_TYPES.CallExpression) {
+            args = node.parent.arguments
+          } else {
+            return
+          }
+        }
+
+        const argument = args[0]
+        if (argument === undefined || argument.type !== 'ObjectExpression') {
+          return
+        }
+
+        const allProperties = argument.properties
+
+        // TODO we need to support spread elements, they would be discarded here
+        const properties = allProperties.flatMap((p) => {
+          if (
+            p.type === AST_NODE_TYPES.Property &&
+            p.key.type === AST_NODE_TYPES.Identifier
+          ) {
+            return { name: p.key.name, property: p }
+          } else if (p.type === AST_NODE_TYPES.SpreadElement) {
+            if (p.argument.type === AST_NODE_TYPES.Identifier) {
+              return { name: p.argument.name, property: p }
+            } else {
+              throw new Error('Unsupported spread element')
+            }
+          }
+          return []
+        })
+
+        const sortedProperties = sortDataByOrder(
+          properties,
+          checkedProperties,
+          'name',
+        )
+        if (sortedProperties === null) {
+          return
+        }
+        context.report({
+          node: argument,
+          data: { function: node.callee.name },
+          messageId: 'invalidOrder',
+          fix(fixer) {
+            const sourceCode = context.sourceCode
+
+            const text = sortedProperties.reduce(
+              (sourceText, specifier, index) => {
+                let text = ''
+                if (index < allProperties.length - 1) {
+                  text = sourceCode
+                    .getText()
+                    .slice(
+                      allProperties[index]!.range[1],
+                      allProperties[index + 1]!.range[0],
+                    )
+                }
+                return (
+                  sourceText + sourceCode.getText(specifier.property) + text
+                )
+              },
+              '',
+            )
+            return fixer.replaceTextRange(
+              [allProperties[0]!.range[0], allProperties.at(-1)!.range[1]],
+              text,
+            )
+          },
+        })
+      },
+    }
+  }),
+})
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
new file mode 100644
index 000000000..0f7d54a74
--- /dev/null
+++ b/packages/eslint-plugin-router/src/rules/create-route-property-order/create-route-property-order.utils.ts
@@ -0,0 +1,38 @@
+export function sortDataByOrder<T, TKey extends keyof T>(
+  data: Array<T> | ReadonlyArray<T>,
+  orderArray: Array<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])!
+
+      return indexA - indexB
+    })
+
+  const inOrderIterator = inOrderArray.values()
+
+  // `as boolean` is needed to avoid TS incorrectly inferring that wasResorted is always `true`
+  let wasResorted = false as boolean
+
+  const result = data.map((item) => {
+    if (orderMap.has(item[key])) {
+      const sortedItem = inOrderIterator.next().value!
+      if (sortedItem[key] !== item[key]) {
+        wasResorted = true
+      }
+      return sortedItem
+    }
+    return item
+  })
+
+  if (!wasResorted) {
+    return null
+  }
+  return result
+}
diff --git a/packages/eslint-plugin-router/src/types.ts b/packages/eslint-plugin-router/src/types.ts
new file mode 100644
index 000000000..74c305318
--- /dev/null
+++ b/packages/eslint-plugin-router/src/types.ts
@@ -0,0 +1,3 @@
+export type ExtraRuleDocs = {
+  recommended: 'strict' | 'error' | 'warn'
+}
diff --git a/packages/eslint-plugin-router/src/utils/detect-router-imports.ts b/packages/eslint-plugin-router/src/utils/detect-router-imports.ts
new file mode 100644
index 000000000..1e9e047da
--- /dev/null
+++ b/packages/eslint-plugin-router/src/utils/detect-router-imports.ts
@@ -0,0 +1,94 @@
+import { TSESTree } from '@typescript-eslint/utils'
+import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'
+
+type Create = Parameters<
+  ReturnType<typeof ESLintUtils.RuleCreator>
+>[0]['create']
+
+type Context = Parameters<Create>[0]
+type Options = Parameters<Create>[1]
+type Helpers = {
+  isSpecificTanstackRouterImport: (
+    node: TSESTree.Identifier,
+    source: string,
+  ) => boolean
+  isTanstackRouterImport: (node: TSESTree.Identifier) => boolean
+}
+
+type EnhancedCreate = (
+  context: Context,
+  options: Options,
+  helpers: Helpers,
+) => ReturnType<Create>
+
+export function detectTanstackRouterImports(create: EnhancedCreate): Create {
+  return (context, optionsWithDefault) => {
+    const tanstackRouterImportSpecifiers: Array<TSESTree.ImportClause> = []
+
+    const helpers: Helpers = {
+      isSpecificTanstackRouterImport(node, source) {
+        return !!tanstackRouterImportSpecifiers.find((specifier) => {
+          if (
+            specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier &&
+            specifier.parent.type ===
+              TSESTree.AST_NODE_TYPES.ImportDeclaration &&
+            specifier.parent.source.value === source
+          ) {
+            return node.name === specifier.local.name
+          }
+
+          return false
+        })
+      },
+      isTanstackRouterImport(node) {
+        return !!tanstackRouterImportSpecifiers.find((specifier) => {
+          if (specifier.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) {
+            return node.name === specifier.local.name
+          }
+
+          return false
+        })
+      },
+    }
+
+    const detectionInstructions: TSESLint.RuleListener = {
+      ImportDeclaration(node) {
+        if (
+          node.specifiers.length > 0 &&
+          node.importKind === 'value' &&
+          node.source.value.startsWith('@tanstack/') &&
+          node.source.value.endsWith('-router')
+        ) {
+          tanstackRouterImportSpecifiers.push(...node.specifiers)
+        }
+      },
+    }
+
+    // Call original rule definition
+    const ruleInstructions = create(context, optionsWithDefault, helpers)
+    const enhancedRuleInstructions: TSESLint.RuleListener = {}
+
+    const allKeys = new Set(
+      Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)),
+    )
+
+    // Iterate over ALL instructions keys so we can override original rule instructions
+    // to prevent their execution if conditions to report errors are not met.
+    allKeys.forEach((instruction) => {
+      enhancedRuleInstructions[instruction] = (node) => {
+        if (instruction in detectionInstructions) {
+          detectionInstructions[instruction]?.(node)
+        }
+
+        const ruleFunction = ruleInstructions[instruction]
+        if (ruleFunction !== undefined) {
+          return ruleFunction(node)
+        }
+
+        return undefined
+      }
+    })
+
+    return enhancedRuleInstructions
+  }
+}
diff --git a/packages/eslint-plugin-router/src/utils/get-docs-url.ts b/packages/eslint-plugin-router/src/utils/get-docs-url.ts
new file mode 100644
index 000000000..8e82192cd
--- /dev/null
+++ b/packages/eslint-plugin-router/src/utils/get-docs-url.ts
@@ -0,0 +1,2 @@
+export const getDocsUrl = (ruleName: string): string =>
+  `https://tanstack.com/router/latest/docs/eslint/${ruleName}`
diff --git a/packages/eslint-plugin-router/tsconfig.json b/packages/eslint-plugin-router/tsconfig.json
new file mode 100644
index 000000000..cbceff3f8
--- /dev/null
+++ b/packages/eslint-plugin-router/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "moduleResolution": "Bundler"
+  },
+  "include": ["src", "eslint.config.js", "vite.config.ts"]
+}
diff --git a/packages/eslint-plugin-router/vite.config.ts b/packages/eslint-plugin-router/vite.config.ts
new file mode 100644
index 000000000..e0de55875
--- /dev/null
+++ b/packages/eslint-plugin-router/vite.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/config/vite'
+import packageJson from './package.json'
+
+const config = defineConfig({
+  test: {
+    name: packageJson.name,
+    dir: './src',
+    watch: false,
+    globals: true,
+    typecheck: { enabled: true },
+    restoreMocks: true,
+  },
+})
+
+export default mergeConfig(
+  config,
+  tanstackViteConfig({
+    entry: './src/index.ts',
+    srcDir: './src',
+    exclude: ['./src/__tests__'],
+  }),
+)
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index c279a6d7d..387cd97c3 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -532,6 +532,15 @@ export const componentTypes = [
   'notFoundComponent',
 ] as const
 
+function routeNeedsPreload(route: AnyRoute) {
+  for (const componentType of componentTypes) {
+    if ((route.options[componentType] as any)?.preload) {
+      return true
+    }
+  }
+  return false
+}
+
 export type RouterEvents = {
   onBeforeNavigate: {
     type: 'onBeforeNavigate'
@@ -1179,7 +1188,10 @@ export class Router<
         }
       } else {
         const status =
-          route.options.loader || route.options.beforeLoad || route.lazyFn
+          route.options.loader ||
+          route.options.beforeLoad ||
+          route.lazyFn ||
+          routeNeedsPreload(route)
             ? 'pending'
             : 'success'
 
diff --git a/packages/router-generator/package.json b/packages/router-generator/package.json
index 82fb4c861..2e3c0673d 100644
--- a/packages/router-generator/package.json
+++ b/packages/router-generator/package.json
@@ -25,6 +25,7 @@
   ],
   "scripts": {
     "clean": "rimraf ./dist && rimraf ./coverage",
+    "clean:snapshots": "rimraf **/*snapshot* --glob",
     "test:eslint": "eslint ./src",
     "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
     "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fe3c63b82..a0a64400f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -22,6 +22,7 @@ overrides:
   '@tanstack/router-arktype-adapter': workspace:*
   '@tanstack/start': workspace:*
   '@tanstack/start-vite-plugin': workspace:*
+  '@tanstack/eslint-plugin-router': workspace:*
   temp-react: 0.0.0-experimental-035a41c4e-20230704
   temp-react-dom: 0.0.0-experimental-035a41c4e-20230704
 
@@ -153,9 +154,6 @@ importers:
       '@tanstack/router-plugin':
         specifier: workspace:*
         version: link:../../../packages/router-plugin
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -185,6 +183,43 @@ importers:
         specifier: ^5.4.5
         version: 5.4.5(@types/node@22.5.4)(terser@5.31.1)
 
+  e2e/react-router/basic-file-based-code-splitting:
+    dependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      '@tanstack/router-devtools':
+        specifier: workspace:*
+        version: link:../../../packages/router-devtools
+      '@tanstack/router-plugin':
+        specifier: workspace:*
+        version: link:../../../packages/router-plugin
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      react-dom:
+        specifier: ^18.2.0
+        version: 18.3.1(react@18.3.1)
+      zod:
+        specifier: ^3.23.8
+        version: 3.23.8
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.47.1
+        version: 1.47.1
+      '@types/react':
+        specifier: ^18.2.47
+        version: 18.3.5
+      '@types/react-dom':
+        specifier: ^18.2.18
+        version: 18.3.0
+      '@vitejs/plugin-react':
+        specifier: ^4.3.1
+        version: 4.3.1(vite@5.4.5(@types/node@22.5.4)(terser@5.31.1))
+      vite:
+        specifier: ^5.4.5
+        version: 5.4.5(@types/node@22.5.4)(terser@5.31.1)
+
   e2e/react-router/basic-react-query:
     dependencies:
       '@tanstack/react-query':
@@ -242,9 +277,6 @@ importers:
       '@tanstack/router-plugin':
         specifier: workspace:*
         version: link:../../../packages/router-plugin
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -288,9 +320,6 @@ importers:
       '@tanstack/virtual-file-routes':
         specifier: workspace:*
         version: link:../../../packages/virtual-file-routes
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -699,9 +728,6 @@ importers:
       '@tanstack/router-plugin':
         specifier: workspace:*
         version: link:../../../packages/router-plugin
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -807,9 +833,6 @@ importers:
       '@tanstack/router-plugin':
         specifier: workspace:*
         version: link:../../../packages/router-plugin
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -847,9 +870,6 @@ importers:
       '@tanstack/router-plugin':
         specifier: workspace:*
         version: link:../../../packages/router-plugin
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -1184,9 +1204,6 @@ importers:
       '@tanstack/router-devtools':
         specifier: workspace:*
         version: link:../../../packages/router-devtools
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -1393,9 +1410,6 @@ importers:
       '@tanstack/router-plugin':
         specifier: workspace:*
         version: link:../../../packages/router-plugin
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -1532,9 +1546,6 @@ importers:
       '@tanstack/router-plugin':
         specifier: workspace:*
         version: link:../../../packages/router-plugin
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -2365,9 +2376,6 @@ importers:
       framer-motion:
         specifier: ^11.5.4
         version: 11.5.4(@emotion/is-prop-valid@0.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      immer:
-        specifier: ^10.1.1
-        version: 10.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -2577,6 +2585,22 @@ importers:
         specifier: ^0.1.0
         version: 0.1.0
 
+  packages/eslint-plugin-router:
+    dependencies:
+      '@typescript-eslint/utils':
+        specifier: ^8.3.0
+        version: 8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)
+    devDependencies:
+      '@typescript-eslint/rule-tester':
+        specifier: ^8.3.0
+        version: 8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)
+      combinate:
+        specifier: ^1.1.11
+        version: 1.1.11
+      eslint:
+        specifier: ^9.10.0
+        version: 9.10.0(jiti@1.21.6)
+
   packages/history: {}
 
   packages/react-cross-context:
@@ -5235,6 +5259,12 @@ packages:
       typescript:
         optional: true
 
+  '@typescript-eslint/rule-tester@8.5.0':
+    resolution: {integrity: sha512-ySLDyc7rktaTLcAICIKf5MG6tjc20g9e6oud4oG1OlsBLZlV9Yl4iuCnmBILojjST8/iYs5o1qebYhF1r97EfA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^9.10.0
+
   '@typescript-eslint/scope-manager@8.3.0':
     resolution: {integrity: sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -12152,6 +12182,19 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/rule-tester@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 8.5.0(typescript@5.6.2)
+      '@typescript-eslint/utils': 8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)
+      ajv: 6.12.6
+      eslint: 9.10.0(jiti@1.21.6)
+      json-stable-stringify-without-jsonify: 1.0.1
+      lodash.merge: 4.6.2
+      semver: 7.6.3
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
   '@typescript-eslint/scope-manager@8.3.0':
     dependencies:
       '@typescript-eslint/types': 8.3.0
diff --git a/scripts/publish.js b/scripts/publish.js
index 83381f3a8..3f2f8b10c 100644
--- a/scripts/publish.js
+++ b/scripts/publish.js
@@ -72,6 +72,10 @@ await publish({
       name: '@tanstack/create-router',
       packageDir: 'packages/create-router',
     },
+    {
+      name: '@tanstack/eslint-plugin-router',
+      packageDir: 'packages/eslint-plugin-router',
+    },
   ],
   branchConfigs: {
     main: {


Test patch:
diff --git a/e2e/react-router/basic-file-based-code-splitting/.gitignore b/e2e/react-router/basic-file-based-code-splitting/.gitignore
new file mode 100644
index 000000000..a6ea47e50
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/e2e/react-router/basic-file-based-code-splitting/index.html b/e2e/react-router/basic-file-based-code-splitting/index.html
new file mode 100644
index 000000000..a4b334943
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/index.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite App</title>
+    <script src="https://cdn.tailwindcss.com"></script>
+    <style type="text/tailwindcss">
+      html {
+        color-scheme: light dark;
+      }
+      * {
+        @apply border-gray-200 dark:border-gray-800;
+      }
+      body {
+        @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>
diff --git a/e2e/react-router/basic-file-based-code-splitting/package.json b/e2e/react-router/basic-file-based-code-splitting/package.json
new file mode 100644
index 000000000..f6fec0dbc
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/package.json
@@ -0,0 +1,27 @@
+{
+  "name": "tanstack-router-e2e-react-basic-file-based-code-splitting",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite --port=3001",
+    "build": "vite build && tsc --noEmit",
+    "serve": "vite preview",
+    "start": "vite",
+    "test:e2e": "playwright test --project=chromium"
+  },
+  "dependencies": {
+    "@tanstack/react-router": "workspace:^",
+    "@tanstack/router-devtools": "workspace:^",
+    "@tanstack/router-plugin": "workspace:^",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "zod": "^3.23.8"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.47.1",
+    "@types/react": "^18.2.47",
+    "@types/react-dom": "^18.2.18",
+    "@vitejs/plugin-react": "^4.3.1",
+    "vite": "^5.4.5"
+  }
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/playwright.config.ts b/e2e/react-router/basic-file-based-code-splitting/playwright.config.ts
new file mode 100644
index 000000000..f83eacf12
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/playwright.config.ts
@@ -0,0 +1,29 @@
+import { defineConfig, devices } from '@playwright/test'
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  testDir: './tests',
+
+  reporter: [['line']],
+
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: 'http://localhost:3001/',
+  },
+
+  webServer: {
+    command: 'pnpm run dev',
+    url: 'http://localhost:3001',
+    reuseExistingServer: !process.env.CI,
+    stdout: 'pipe',
+  },
+
+  projects: [
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+  ],
+})
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/main.tsx b/e2e/react-router/basic-file-based-code-splitting/src/main.tsx
new file mode 100644
index 000000000..18b1b603f
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/main.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { RouterProvider, createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+
+// Set up a Router instance
+const router = createRouter({
+  routeTree,
+  defaultPreload: 'intent',
+  defaultStaleTime: 5000,
+})
+
+// Register things for typesafety
+declare module '@tanstack/react-router' {
+  interface Register {
+    router: typeof router
+  }
+}
+
+const rootElement = document.getElementById('app')!
+
+if (!rootElement.innerHTML) {
+  const root = ReactDOM.createRoot(rootElement)
+  root.render(<RouterProvider router={router} />)
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/posts.tsx b/e2e/react-router/basic-file-based-code-splitting/src/posts.tsx
new file mode 100644
index 000000000..3ccf1ff42
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/posts.tsx
@@ -0,0 +1,32 @@
+import { notFound } from '@tanstack/react-router'
+import axios from 'redaxios'
+
+export type PostType = {
+  id: string
+  title: string
+  body: string
+}
+
+export const fetchPost = async (postId: string) => {
+  console.info(`Fetching post with id ${postId}...`)
+  await new Promise((r) => setTimeout(r, 500))
+  const post = await axios
+    .get<PostType>(`https://jsonplaceholder.typicode.com/posts/${postId}`)
+    .then((r) => r.data)
+    .catch((err) => {
+      if (err.status === 404) {
+        throw notFound()
+      }
+      throw err
+    })
+
+  return post
+}
+
+export const fetchPosts = async () => {
+  console.info('Fetching posts...')
+  await new Promise((r) => setTimeout(r, 500))
+  return axios
+    .get<Array<PostType>>('https://jsonplaceholder.typicode.com/posts')
+    .then((r) => r.data.slice(0, 10))
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routeTree.gen.ts b/e2e/react-router/basic-file-based-code-splitting/src/routeTree.gen.ts
new file mode 100644
index 000000000..b5bbbfc90
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routeTree.gen.ts
@@ -0,0 +1,325 @@
+/* prettier-ignore-start */
+
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file is auto-generated by TanStack Router
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as WithoutLoaderImport } from './routes/without-loader'
+import { Route as PostsImport } from './routes/posts'
+import { Route as LayoutImport } from './routes/_layout'
+import { Route as IndexImport } from './routes/index'
+import { Route as PostsIndexImport } from './routes/posts.index'
+import { Route as PostsPostIdImport } from './routes/posts.$postId'
+import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2'
+import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b'
+import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a'
+
+// Create/Update Routes
+
+const WithoutLoaderRoute = WithoutLoaderImport.update({
+  path: '/without-loader',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const PostsRoute = PostsImport.update({
+  path: '/posts',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const LayoutRoute = LayoutImport.update({
+  id: '/_layout',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const IndexRoute = IndexImport.update({
+  path: '/',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const PostsIndexRoute = PostsIndexImport.update({
+  path: '/',
+  getParentRoute: () => PostsRoute,
+} as any)
+
+const PostsPostIdRoute = PostsPostIdImport.update({
+  path: '/$postId',
+  getParentRoute: () => PostsRoute,
+} as any)
+
+const LayoutLayout2Route = LayoutLayout2Import.update({
+  id: '/_layout-2',
+  getParentRoute: () => LayoutRoute,
+} as any)
+
+const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({
+  path: '/layout-b',
+  getParentRoute: () => LayoutLayout2Route,
+} as any)
+
+const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({
+  path: '/layout-a',
+  getParentRoute: () => LayoutLayout2Route,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/react-router' {
+  interface FileRoutesByPath {
+    '/': {
+      id: '/'
+      path: '/'
+      fullPath: '/'
+      preLoaderRoute: typeof IndexImport
+      parentRoute: typeof rootRoute
+    }
+    '/_layout': {
+      id: '/_layout'
+      path: ''
+      fullPath: ''
+      preLoaderRoute: typeof LayoutImport
+      parentRoute: typeof rootRoute
+    }
+    '/posts': {
+      id: '/posts'
+      path: '/posts'
+      fullPath: '/posts'
+      preLoaderRoute: typeof PostsImport
+      parentRoute: typeof rootRoute
+    }
+    '/without-loader': {
+      id: '/without-loader'
+      path: '/without-loader'
+      fullPath: '/without-loader'
+      preLoaderRoute: typeof WithoutLoaderImport
+      parentRoute: typeof rootRoute
+    }
+    '/_layout/_layout-2': {
+      id: '/_layout/_layout-2'
+      path: ''
+      fullPath: ''
+      preLoaderRoute: typeof LayoutLayout2Import
+      parentRoute: typeof LayoutImport
+    }
+    '/posts/$postId': {
+      id: '/posts/$postId'
+      path: '/$postId'
+      fullPath: '/posts/$postId'
+      preLoaderRoute: typeof PostsPostIdImport
+      parentRoute: typeof PostsImport
+    }
+    '/posts/': {
+      id: '/posts/'
+      path: '/'
+      fullPath: '/posts/'
+      preLoaderRoute: typeof PostsIndexImport
+      parentRoute: typeof PostsImport
+    }
+    '/_layout/_layout-2/layout-a': {
+      id: '/_layout/_layout-2/layout-a'
+      path: '/layout-a'
+      fullPath: '/layout-a'
+      preLoaderRoute: typeof LayoutLayout2LayoutAImport
+      parentRoute: typeof LayoutLayout2Import
+    }
+    '/_layout/_layout-2/layout-b': {
+      id: '/_layout/_layout-2/layout-b'
+      path: '/layout-b'
+      fullPath: '/layout-b'
+      preLoaderRoute: typeof LayoutLayout2LayoutBImport
+      parentRoute: typeof LayoutLayout2Import
+    }
+  }
+}
+
+// Create and export the route tree
+
+interface LayoutLayout2RouteChildren {
+  LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
+  LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
+}
+
+const LayoutLayout2RouteChildren: LayoutLayout2RouteChildren = {
+  LayoutLayout2LayoutARoute: LayoutLayout2LayoutARoute,
+  LayoutLayout2LayoutBRoute: LayoutLayout2LayoutBRoute,
+}
+
+const LayoutLayout2RouteWithChildren = LayoutLayout2Route._addFileChildren(
+  LayoutLayout2RouteChildren,
+)
+
+interface LayoutRouteChildren {
+  LayoutLayout2Route: typeof LayoutLayout2RouteWithChildren
+}
+
+const LayoutRouteChildren: LayoutRouteChildren = {
+  LayoutLayout2Route: LayoutLayout2RouteWithChildren,
+}
+
+const LayoutRouteWithChildren =
+  LayoutRoute._addFileChildren(LayoutRouteChildren)
+
+interface PostsRouteChildren {
+  PostsPostIdRoute: typeof PostsPostIdRoute
+  PostsIndexRoute: typeof PostsIndexRoute
+}
+
+const PostsRouteChildren: PostsRouteChildren = {
+  PostsPostIdRoute: PostsPostIdRoute,
+  PostsIndexRoute: PostsIndexRoute,
+}
+
+const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
+
+export interface FileRoutesByFullPath {
+  '/': typeof IndexRoute
+  '': typeof LayoutLayout2RouteWithChildren
+  '/posts': typeof PostsRouteWithChildren
+  '/without-loader': typeof WithoutLoaderRoute
+  '/posts/$postId': typeof PostsPostIdRoute
+  '/posts/': typeof PostsIndexRoute
+  '/layout-a': typeof LayoutLayout2LayoutARoute
+  '/layout-b': typeof LayoutLayout2LayoutBRoute
+}
+
+export interface FileRoutesByTo {
+  '/': typeof IndexRoute
+  '': typeof LayoutLayout2RouteWithChildren
+  '/without-loader': typeof WithoutLoaderRoute
+  '/posts/$postId': typeof PostsPostIdRoute
+  '/posts': typeof PostsIndexRoute
+  '/layout-a': typeof LayoutLayout2LayoutARoute
+  '/layout-b': typeof LayoutLayout2LayoutBRoute
+}
+
+export interface FileRoutesById {
+  __root__: typeof rootRoute
+  '/': typeof IndexRoute
+  '/_layout': typeof LayoutRouteWithChildren
+  '/posts': typeof PostsRouteWithChildren
+  '/without-loader': typeof WithoutLoaderRoute
+  '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
+  '/posts/$postId': typeof PostsPostIdRoute
+  '/posts/': typeof PostsIndexRoute
+  '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
+  '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
+}
+
+export interface FileRouteTypes {
+  fileRoutesByFullPath: FileRoutesByFullPath
+  fullPaths:
+    | '/'
+    | ''
+    | '/posts'
+    | '/without-loader'
+    | '/posts/$postId'
+    | '/posts/'
+    | '/layout-a'
+    | '/layout-b'
+  fileRoutesByTo: FileRoutesByTo
+  to:
+    | '/'
+    | ''
+    | '/without-loader'
+    | '/posts/$postId'
+    | '/posts'
+    | '/layout-a'
+    | '/layout-b'
+  id:
+    | '__root__'
+    | '/'
+    | '/_layout'
+    | '/posts'
+    | '/without-loader'
+    | '/_layout/_layout-2'
+    | '/posts/$postId'
+    | '/posts/'
+    | '/_layout/_layout-2/layout-a'
+    | '/_layout/_layout-2/layout-b'
+  fileRoutesById: FileRoutesById
+}
+
+export interface RootRouteChildren {
+  IndexRoute: typeof IndexRoute
+  LayoutRoute: typeof LayoutRouteWithChildren
+  PostsRoute: typeof PostsRouteWithChildren
+  WithoutLoaderRoute: typeof WithoutLoaderRoute
+}
+
+const rootRouteChildren: RootRouteChildren = {
+  IndexRoute: IndexRoute,
+  LayoutRoute: LayoutRouteWithChildren,
+  PostsRoute: PostsRouteWithChildren,
+  WithoutLoaderRoute: WithoutLoaderRoute,
+}
+
+export const routeTree = rootRoute
+  ._addFileChildren(rootRouteChildren)
+  ._addFileTypes<FileRouteTypes>()
+
+/* prettier-ignore-end */
+
+/* ROUTE_MANIFEST_START
+{
+  "routes": {
+    "__root__": {
+      "filePath": "__root.tsx",
+      "children": [
+        "/",
+        "/_layout",
+        "/posts",
+        "/without-loader"
+      ]
+    },
+    "/": {
+      "filePath": "index.tsx"
+    },
+    "/_layout": {
+      "filePath": "_layout.tsx",
+      "children": [
+        "/_layout/_layout-2"
+      ]
+    },
+    "/posts": {
+      "filePath": "posts.tsx",
+      "children": [
+        "/posts/$postId",
+        "/posts/"
+      ]
+    },
+    "/without-loader": {
+      "filePath": "without-loader.tsx"
+    },
+    "/_layout/_layout-2": {
+      "filePath": "_layout/_layout-2.tsx",
+      "parent": "/_layout",
+      "children": [
+        "/_layout/_layout-2/layout-a",
+        "/_layout/_layout-2/layout-b"
+      ]
+    },
+    "/posts/$postId": {
+      "filePath": "posts.$postId.tsx",
+      "parent": "/posts"
+    },
+    "/posts/": {
+      "filePath": "posts.index.tsx",
+      "parent": "/posts"
+    },
+    "/_layout/_layout-2/layout-a": {
+      "filePath": "_layout/_layout-2/layout-a.tsx",
+      "parent": "/_layout/_layout-2"
+    },
+    "/_layout/_layout-2/layout-b": {
+      "filePath": "_layout/_layout-2/layout-b.tsx",
+      "parent": "/_layout/_layout-2"
+    }
+  }
+}
+ROUTE_MANIFEST_END */
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/__root.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/__root.tsx
new file mode 100644
index 000000000..f4dca5da2
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/__root.tsx
@@ -0,0 +1,70 @@
+import * as React from 'react'
+import { Link, Outlet, createRootRoute } from '@tanstack/react-router'
+import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export const Route = createRootRoute({
+  component: RootComponent,
+  notFoundComponent: () => {
+    return (
+      <div>
+        <p>This is the notFoundComponent configured on root route</p>
+        <Link to="/">Start Over</Link>
+      </div>
+    )
+  },
+})
+
+function RootComponent() {
+  return (
+    <>
+      <div className="p-2 flex gap-2 text-lg border-b">
+        <Link
+          to="/"
+          activeProps={{
+            className: 'font-bold',
+          }}
+          activeOptions={{ exact: true }}
+        >
+          Home
+        </Link>{' '}
+        <Link
+          to="/posts"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          Posts
+        </Link>{' '}
+        <Link
+          to="/layout-a"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          Layout
+        </Link>{' '}
+        <Link
+          to="/without-loader"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          without-loader
+        </Link>{' '}
+        <Link
+          // @ts-expect-error
+          to="/this-route-does-not-exist"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          This Route Does Not Exist
+        </Link>
+      </div>
+      <hr />
+      <Outlet />
+      {/* Start rendering router matches */}
+      <TanStackRouterDevtools position="bottom-right" />
+    </>
+  )
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout.tsx
new file mode 100644
index 000000000..02ddbb1cd
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout.tsx
@@ -0,0 +1,16 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout')({
+  component: LayoutComponent,
+})
+
+function LayoutComponent() {
+  return (
+    <div className="p-2">
+      <div className="border-b">I'm a layout</div>
+      <div>
+        <Outlet />
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2.tsx
new file mode 100644
index 000000000..3b7dbf290
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2.tsx
@@ -0,0 +1,34 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout/_layout-2')({
+  component: LayoutComponent,
+})
+
+function LayoutComponent() {
+  return (
+    <div>
+      <div>I'm a nested layout</div>
+      <div className="flex gap-2 border-b">
+        <Link
+          to="/layout-a"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          Layout A
+        </Link>
+        <Link
+          to="/layout-b"
+          activeProps={{
+            className: 'font-bold',
+          }}
+        >
+          Layout B
+        </Link>
+      </div>
+      <div>
+        <Outlet />
+      </div>
+    </div>
+  )
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-a.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-a.tsx
new file mode 100644
index 000000000..61e19b4d9
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-a.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-a')({
+  component: LayoutAComponent,
+})
+
+function LayoutAComponent() {
+  return <div>I'm layout A!</div>
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-b.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-b.tsx
new file mode 100644
index 000000000..cceed1fb9
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/_layout/_layout-2/layout-b.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/_layout/_layout-2/layout-b')({
+  component: LayoutBComponent,
+})
+
+function LayoutBComponent() {
+  return <div>I'm layout B!</div>
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/index.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/index.tsx
new file mode 100644
index 000000000..eac82a917
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/index.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+  component: Home,
+})
+
+function Home() {
+  return (
+    <div className="p-2">
+      <h3>Welcome Home!</h3>
+    </div>
+  )
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.$postId.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.$postId.tsx
new file mode 100644
index 000000000..febb02ab2
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.$postId.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react'
+import { ErrorComponent, createFileRoute } from '@tanstack/react-router'
+import { fetchPost } from '../posts'
+import type { ErrorComponentProps } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/posts/$postId')({
+  loader: async ({ params: { postId } }) => fetchPost(postId),
+  errorComponent: PostErrorComponent as any,
+  notFoundComponent: () => {
+    return <p>Post not found</p>
+  },
+  component: PostComponent,
+})
+
+export function PostErrorComponent({ error }: ErrorComponentProps) {
+  return <ErrorComponent error={error} />
+}
+
+function PostComponent() {
+  const post = Route.useLoaderData()
+
+  return (
+    <div className="space-y-2">
+      <h4 className="text-xl font-bold underline">{post.title}</h4>
+      <div className="text-sm">{post.body}</div>
+    </div>
+  )
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.index.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.index.tsx
new file mode 100644
index 000000000..056433ca0
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.index.tsx
@@ -0,0 +1,10 @@
+import * as React from 'react'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/posts/')({
+  component: PostsIndexComponent,
+})
+
+function PostsIndexComponent() {
+  return <div>Select a post.</div>
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.tsx
new file mode 100644
index 000000000..c7a09ed7f
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/posts.tsx
@@ -0,0 +1,39 @@
+import * as React from 'react'
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+import { fetchPosts } from '../posts'
+
+export const Route = createFileRoute('/posts')({
+  loader: fetchPosts,
+  component: PostsComponent,
+})
+
+function PostsComponent() {
+  const posts = Route.useLoaderData()
+
+  return (
+    <div className="p-2 flex gap-2">
+      <ul className="list-disc pl-4">
+        {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map(
+          (post) => {
+            return (
+              <li key={post.id} className="whitespace-nowrap">
+                <Link
+                  to="/posts/$postId"
+                  params={{
+                    postId: post.id,
+                  }}
+                  className="block py-1 text-blue-600 hover:opacity-75"
+                  activeProps={{ className: 'font-bold underline' }}
+                >
+                  <div>{post.title.substring(0, 20)}</div>
+                </Link>
+              </li>
+            )
+          },
+        )}
+      </ul>
+      <hr />
+      <Outlet />
+    </div>
+  )
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/src/routes/without-loader.tsx b/e2e/react-router/basic-file-based-code-splitting/src/routes/without-loader.tsx
new file mode 100644
index 000000000..9dddcfe5f
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/src/routes/without-loader.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/without-loader')({
+  component: () => <div>Hello /without-loader!</div>,
+})
diff --git a/e2e/react-router/basic-file-based-code-splitting/tests/app.spec.ts b/e2e/react-router/basic-file-based-code-splitting/tests/app.spec.ts
new file mode 100644
index 000000000..f19a88b56
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/tests/app.spec.ts
@@ -0,0 +1,34 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+  await page.goto('/')
+})
+
+test('Navigating to a post page', async ({ page }) => {
+  await page.getByRole('link', { name: 'Posts' }).click()
+  await page.getByRole('link', { name: 'sunt aut facere repe' }).click()
+  await expect(page.getByRole('heading')).toContainText('sunt aut facere')
+})
+
+test('Navigating nested layouts', async ({ page }) => {
+  await page.goto('/')
+  await page.getByRole('link', { name: 'Layout', exact: true }).click()
+
+  await expect(page.locator('#app')).toContainText("I'm a layout")
+  await expect(page.locator('#app')).toContainText("I'm a nested layout")
+
+  await page.getByRole('link', { name: 'Layout A' }).click()
+  await expect(page.locator('#app')).toContainText("I'm layout A!")
+
+  await page.getByRole('link', { name: 'Layout B' }).click()
+  await expect(page.locator('#app')).toContainText("I'm layout B!")
+})
+
+test('Navigating to a not-found route', async ({ page }) => {
+  await page.getByRole('link', { name: 'This Route Does Not Exist' }).click()
+  await expect(page.getByRole('paragraph')).toContainText(
+    'This is the notFoundComponent configured on root route',
+  )
+  await page.getByRole('link', { name: 'Start Over' }).click()
+  await expect(page.getByRole('heading')).toContainText('Welcome Home!')
+})
diff --git a/e2e/react-router/basic-file-based-code-splitting/tests/preload.test.ts b/e2e/react-router/basic-file-based-code-splitting/tests/preload.test.ts
new file mode 100644
index 000000000..e1334df0d
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/tests/preload.test.ts
@@ -0,0 +1,21 @@
+import { expect, test } from '@playwright/test'
+
+test.beforeEach(async ({ page }) => {
+  await page.goto('/')
+})
+
+test('hovering a link with preload=intent to a route without a loader should preload route', async ({
+  page,
+}) => {
+  await page.waitForLoadState('networkidle')
+
+  const requestPromise = new Promise<string>((resolve) => {
+    page.on('request', (request) => {
+      resolve(request.url())
+    })
+  })
+
+  await page.getByRole('link', { name: 'without-loader' }).hover()
+  const url = await requestPromise
+  expect(url).toContain('without-loader.tsx?tsr-split')
+})
diff --git a/e2e/react-router/basic-file-based-code-splitting/tsconfig.json b/e2e/react-router/basic-file-based-code-splitting/tsconfig.json
new file mode 100644
index 000000000..3435721cc
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/tsconfig.json
@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx",
+    "target": "ESNext",
+    "moduleResolution": "Bundler",
+    "noEmit": true,
+    "skipLibCheck": true
+  }
+}
diff --git a/e2e/react-router/basic-file-based-code-splitting/vite.config.ts b/e2e/react-router/basic-file-based-code-splitting/vite.config.ts
new file mode 100644
index 000000000..3cc73e272
--- /dev/null
+++ b/e2e/react-router/basic-file-based-code-splitting/vite.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [TanStackRouterVite({ autoCodeSplitting: true }), react()],
+})
diff --git a/e2e/react-router/basic-file-based/package.json b/e2e/react-router/basic-file-based/package.json
index baccfe13d..76f565d77 100644
--- a/e2e/react-router/basic-file-based/package.json
+++ b/e2e/react-router/basic-file-based/package.json
@@ -13,7 +13,6 @@
     "@tanstack/react-router": "workspace:^",
     "@tanstack/router-devtools": "workspace:^",
     "@tanstack/router-plugin": "workspace:^",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/e2e/react-router/basic-react-query-file-based/package.json b/e2e/react-router/basic-react-query-file-based/package.json
index 184ac95ac..1476b96c2 100644
--- a/e2e/react-router/basic-react-query-file-based/package.json
+++ b/e2e/react-router/basic-react-query-file-based/package.json
@@ -15,7 +15,6 @@
     "@tanstack/react-router": "workspace:^",
     "@tanstack/router-devtools": "workspace:^",
     "@tanstack/router-plugin": "workspace:^",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
diff --git a/e2e/react-router/basic-virtual-file-based/package.json b/e2e/react-router/basic-virtual-file-based/package.json
index b7857ce83..b05ac2037 100644
--- a/e2e/react-router/basic-virtual-file-based/package.json
+++ b/e2e/react-router/basic-virtual-file-based/package.json
@@ -14,7 +14,6 @@
     "@tanstack/router-devtools": "workspace:^",
     "@tanstack/router-plugin": "workspace:^",
     "@tanstack/virtual-file-routes": "workspace:^",
-    "immer": "^10.1.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1",
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
new file mode 100644
index 000000000..139bd1710
--- /dev/null
+++ b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.rule.test.ts
@@ -0,0 +1,153 @@
+import { RuleTester } from '@typescript-eslint/rule-tester'
+import combinate from 'combinate'
+import {
+  name,
+  rule,
+} from '../rules/create-route-property-order/create-route-property-order.rule'
+import {
+  checkedProperties,
+  createRouteFunctionsDirect,
+  createRouteFunctionsIndirect,
+} from '../rules/create-route-property-order/constants'
+import {
+  generateInterleavedCombinations,
+  generatePartialCombinations,
+  generatePermutations,
+  normalizeIndent,
+} from './test-utils'
+
+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],
+]
+type TestedCheckedProperties = (typeof testedCheckedProperties)[number]
+const orderIndependentProps = ['gcTime', '...foo'] as const
+type OrderIndependentProps = (typeof orderIndependentProps)[number]
+
+// reduce the number of test cases by only testing the first function of createRouteFunctionsDirect
+const testedCreateRouteFunctions = [
+  ...createRouteFunctionsIndirect,
+  createRouteFunctionsDirect[0],
+]
+type TestedCreateRouteFunction = (typeof testedCreateRouteFunctions)[number]
+
+interface TestCase {
+  createRouteFunction: TestedCreateRouteFunction
+  properties: Array<TestedCheckedProperties | OrderIndependentProps>
+}
+
+const validTestMatrix = combinate({
+  createRouteFunction: testedCreateRouteFunctions,
+  properties: generatePartialCombinations(testedCheckedProperties, 2),
+})
+
+export function generateInvalidPermutations<T>(
+  arr: ReadonlyArray<T>,
+): Array<{ invalid: Array<T>; valid: Array<T> }> {
+  const combinations = generatePartialCombinations(arr, 2)
+  const allPermutations: Array<{ invalid: Array<T>; valid: Array<T> }> = []
+
+  for (const combination of combinations) {
+    const permutations = generatePermutations(combination)
+    // skip the first permutation as it matches the original combination
+    const invalidPermutations = permutations.slice(1)
+    allPermutations.push(
+      ...invalidPermutations.map((p) => ({ invalid: p, valid: combination })),
+    )
+  }
+
+  return allPermutations
+}
+
+const invalidPermutations = generateInvalidPermutations(testedCheckedProperties)
+
+type Interleaved = TestedCheckedProperties | OrderIndependentProps
+const interleavedInvalidPermutations: Array<{
+  invalid: Array<Interleaved>
+  valid: Array<Interleaved>
+}> = []
+for (const invalidPermutation of invalidPermutations) {
+  const invalid = generateInterleavedCombinations(
+    invalidPermutation.invalid,
+    orderIndependentProps,
+  )
+  const valid = generateInterleavedCombinations(
+    invalidPermutation.valid,
+    orderIndependentProps,
+  )
+
+  for (let i = 0; i < invalid.length; i++) {
+    interleavedInvalidPermutations.push({
+      invalid: invalid[i]!,
+      valid: valid[i]!,
+    })
+  }
+}
+
+const invalidTestMatrix = combinate({
+  createRouteFunction: testedCreateRouteFunctions,
+  properties: interleavedInvalidPermutations,
+})
+
+function getCode({ createRouteFunction, properties }: TestCase) {
+  let invocation = ''
+  switch (createRouteFunction) {
+    case 'createFileRoute': {
+      invocation = `('/_layout/hello/foo/$id')`
+      break
+    }
+    case 'createRootRouteWithContext': {
+      invocation = normalizeIndent`
+      <{
+          queryClient: QueryClient
+       }>()`
+      break
+    }
+  }
+  function getPropertyCode(
+    property: TestedCheckedProperties | OrderIndependentProps,
+  ) {
+    if (property.startsWith('...')) {
+      return property
+    }
+    return `${property}: () => null`
+  }
+  return `
+    import { ${createRouteFunction} } from '@tanstack/react-router'
+
+    const Route = ${createRouteFunction}${invocation}({
+        ${properties.map(getPropertyCode).join(',\n        ')}
+    })
+  `
+}
+
+const validTestCases = validTestMatrix.map(
+  ({ createRouteFunction, properties }) => ({
+    name: `should pass when order is correct for ${createRouteFunction} with order: ${properties.join(', ')}`,
+    code: getCode({ createRouteFunction, properties }),
+  }),
+)
+
+const invalidTestCases = invalidTestMatrix.map(
+  ({ createRouteFunction, properties }) => ({
+    name: `incorrect property order is detected for ${createRouteFunction} with order: ${properties.invalid.join(', ')}`,
+    code: getCode({
+      createRouteFunction,
+      properties: properties.invalid,
+    }),
+    errors: [{ messageId: 'invalidOrder' }],
+    output: getCode({
+      createRouteFunction,
+      properties: properties.valid,
+    }),
+  }),
+)
+
+ruleTester.run(name, rule, {
+  valid: validTestCases,
+  invalid: invalidTestCases,
+})
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
new file mode 100644
index 000000000..b375ebe3e
--- /dev/null
+++ b/packages/eslint-plugin-router/src/__tests__/create-route-property-order.utils.test.ts
@@ -0,0 +1,58 @@
+import { describe, expect, test } from 'vitest'
+import { sortDataByOrder } from '../rules/create-route-property-order/create-route-property-order.utils'
+
+describe('create-route-property-order utils', () => {
+  describe('sortDataByOrder', () => {
+    const testCases = [
+      {
+        data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }],
+        orderArray: ['a', 'b', 'c'],
+        key: 'key',
+        expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
+      },
+      {
+        data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }],
+        orderArray: ['a', 'b', 'c'],
+        key: 'key',
+        expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
+      },
+      {
+        data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
+        orderArray: ['a', 'b', 'c'],
+        key: 'key',
+        expected: null,
+      },
+      {
+        data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }],
+        orderArray: ['a', 'b', 'c'],
+        key: 'key',
+        expected: null,
+      },
+      {
+        data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }],
+        orderArray: ['a', 'b', 'c'],
+        key: 'key',
+        expected: null,
+      },
+      {
+        data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
+        orderArray: ['a', 'b', 'c'],
+        key: 'key',
+        expected: null,
+      },
+      {
+        data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
+        orderArray: ['a', 'b', 'c'],
+        key: 'key',
+        expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
+      },
+    ] as const
+    test.each(testCases)(
+      '$data $orderArray $key $expected',
+      ({ data, orderArray, key, expected }) => {
+        const sortedData = sortDataByOrder(data, orderArray, key)
+        expect(sortedData).toEqual(expected)
+      },
+    )
+  })
+})
diff --git a/packages/eslint-plugin-router/src/__tests__/test-utils.test.ts b/packages/eslint-plugin-router/src/__tests__/test-utils.test.ts
new file mode 100644
index 000000000..aac231ebd
--- /dev/null
+++ b/packages/eslint-plugin-router/src/__tests__/test-utils.test.ts
@@ -0,0 +1,104 @@
+import { describe, expect, test } from 'vitest'
+import {
+  expectArrayEqualIgnoreOrder,
+  generateInterleavedCombinations,
+  generatePartialCombinations,
+  generatePermutations,
+} from './test-utils'
+
+describe('test-utils', () => {
+  describe('generatePermutations', () => {
+    const testCases = [
+      {
+        input: ['a', 'b', 'c'],
+        expected: [
+          ['a', 'b', 'c'],
+          ['a', 'c', 'b'],
+          ['b', 'a', 'c'],
+          ['b', 'c', 'a'],
+          ['c', 'a', 'b'],
+          ['c', 'b', 'a'],
+        ],
+      },
+      {
+        input: ['a', 'b'],
+        expected: [
+          ['a', 'b'],
+          ['b', 'a'],
+        ],
+      },
+      {
+        input: ['a'],
+        expected: [['a']],
+      },
+    ]
+    test.each(testCases)('$input $expected', ({ input, expected }) => {
+      const permutations = generatePermutations(input)
+      expect(permutations).toEqual(expected)
+    })
+  })
+
+  describe('generatePartialCombinations', () => {
+    const testCases = [
+      {
+        input: ['a', 'b', 'c'],
+        minLength: 2,
+        expected: [
+          ['a', 'b'],
+          ['a', 'c'],
+          ['b', 'c'],
+          ['a', 'b', 'c'],
+        ],
+      },
+      {
+        input: ['a', 'b'],
+        expected: [['a', 'b']],
+        minLength: 2,
+      },
+      {
+        input: ['a'],
+        expected: [],
+        minLength: 2,
+      },
+      {
+        input: ['a'],
+        expected: [['a']],
+        minLength: 1,
+      },
+      {
+        input: ['a'],
+        expected: [[], ['a']],
+        minLength: 0,
+      },
+    ]
+    test.each(testCases)(
+      '$input $minLength $expected ',
+      ({ input, minLength, expected }) => {
+        const combinations = generatePartialCombinations(input, minLength)
+        expectArrayEqualIgnoreOrder(combinations, expected)
+      },
+    )
+  })
+
+  describe('generateInterleavedCombinations', () => {
+    const testCases = [
+      {
+        data: ['a', 'b'],
+        additional: ['x'],
+        expected: [
+          ['a', 'b'],
+          ['x', 'a', 'b'],
+          ['a', 'x', 'b'],
+          ['a', 'b', 'x'],
+        ],
+      },
+    ]
+    test.each(testCases)(
+      '$input $expected',
+      ({ data, additional, expected }) => {
+        const combinations = generateInterleavedCombinations(data, additional)
+        expectArrayEqualIgnoreOrder(combinations, expected)
+      },
+    )
+  })
+})
diff --git a/packages/eslint-plugin-router/src/__tests__/test-utils.ts b/packages/eslint-plugin-router/src/__tests__/test-utils.ts
new file mode 100644
index 000000000..63a2f514f
--- /dev/null
+++ b/packages/eslint-plugin-router/src/__tests__/test-utils.ts
@@ -0,0 +1,108 @@
+import { expect } from 'vitest'
+
+export function generatePermutations<T>(arr: Array<T>): Array<Array<T>> {
+  if (arr.length <= 1) {
+    return [arr]
+  }
+
+  const result: Array<Array<T>> = []
+  for (let i = 0; i < arr.length; i++) {
+    const rest = arr.slice(0, i).concat(arr.slice(i + 1))
+    const restPermutations = generatePermutations(rest)
+    for (const perm of restPermutations) {
+      result.push([arr[i]!, ...perm])
+    }
+  }
+
+  return result
+}
+
+export function generatePartialCombinations<T>(
+  arr: ReadonlyArray<T>,
+  minLength: number,
+): Array<Array<T>> {
+  const result: Array<Array<T>> = []
+
+  function backtrack(start: number, current: Array<T>) {
+    if (current.length > minLength - 1) {
+      result.push([...current])
+    }
+    for (let i = start; i < arr.length; i++) {
+      current.push(arr[i]!)
+      backtrack(i + 1, current)
+      current.pop()
+    }
+  }
+  backtrack(0, [])
+  return result
+}
+
+export function expectArrayEqualIgnoreOrder<T>(a: Array<T>, b: Array<T>) {
+  expect([...a].sort()).toEqual([...b].sort())
+}
+
+export function normalizeIndent(template: TemplateStringsArray) {
+  const codeLines = template[0]?.split('\n') ?? ['']
+  const leftPadding = codeLines[1]?.match(/\s+/)?.[0] ?? ''
+  return codeLines.map((line) => line.slice(leftPadding.length)).join('\n')
+}
+
+export function generateInterleavedCombinations<
+  TData,
+  TAdditional,
+  TResult extends TData | TAdditional,
+>(
+  data: Array<TData> | ReadonlyArray<TData>,
+  additional: Array<TAdditional> | ReadonlyArray<TAdditional>,
+): Array<Array<TResult>> {
+  const result: Array<Array<TResult>> = []
+
+  function getSubsets(array: Array<TAdditional>): Array<Array<TAdditional>> {
+    return array.reduce(
+      (subsets, value) => {
+        return subsets.concat(subsets.map((set) => [...set, value]))
+      },
+      [[]] as Array<Array<TAdditional>>,
+    )
+  }
+
+  function insertAtPositions(
+    data: Array<TResult>,
+    subset: Array<TResult>,
+  ): Array<Array<TResult>> {
+    const combinations: Array<Array<TResult>> = []
+
+    const recurse = (
+      currentData: Array<TResult>,
+      currentSubset: Array<TResult>,
+      start: number,
+    ): void => {
+      if (currentSubset.length === 0) {
+        combinations.push([...currentData])
+        return
+      }
+
+      for (let i = start; i <= currentData.length; i++) {
+        const newData = [
+          ...currentData.slice(0, i),
+          currentSubset[0]!,
+          ...currentData.slice(i),
+        ]
+        recurse(newData, currentSubset.slice(1), i + 1)
+      }
+    }
+
+    recurse(data, subset, 0)
+    return combinations
+  }
+
+  const subsets = getSubsets(additional as Array<TAdditional>)
+
+  subsets.forEach((subset) => {
+    result.push(
+      ...insertAtPositions(data as Array<TResult>, subset as Array<TResult>),
+    )
+  })
+
+  return result
+}
diff --git a/packages/router-generator/tests/generator/file-modification/snapshot/empty.lazy.tsx b/packages/router-generator/tests/generator/file-modification/snapshot/empty.lazy.tsx
deleted file mode 100644
index d8a6feca9..000000000
--- a/packages/router-generator/tests/generator/file-modification/snapshot/empty.lazy.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-// @ts-nocheck
-
-import * as React from 'react'
-import {
-  ErrorComponent,
-  ErrorComponentProps,
-  Link,
-  createFileRoute,
-} from '@tanstack/react-router'
-
-export const Route = createFileRoute('/(test)/foo')({
-  loader: async ({ params: { postId } }) => ({
-    postId,
-    title: 'title',
-    body: 'body',
-    id: 'id',
-  }),
-  errorComponent: PostErrorComponent as any,
-  notFoundComponent: () => {
-    return <p>Post not found</p>
-  },
-  component: PostComponent,
-})
-
-export function PostErrorComponent({ error }: ErrorComponentProps) {
-  return <ErrorComponent error={error} />
-}
-
-function PostComponent() {
-  const post = Route.useLoaderData()
-
-  return (
-    <div className="space-y-2">
-      <h4 className="text-xl font-bold underline">{post.title}</h4>
-      <div className="text-sm">{post.body}</div>
-      <Link
-        to="/posts/$postId/deep"
-        params={{
-          postId: post.id,
-        }}
-        activeProps={{ className: 'text-black font-bold' }}
-        className="block py-1 text-blue-800 hover:text-blue-600"
-      >
-        Deep View
-      </Link>
-    </div>
-  )
-}
diff --git a/packages/router-generator/tests/generator/file-modification/snapshot/empty.tsx b/packages/router-generator/tests/generator/file-modification/snapshot/empty.tsx
deleted file mode 100644
index d8a6feca9..000000000
--- a/packages/router-generator/tests/generator/file-modification/snapshot/empty.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-// @ts-nocheck
-
-import * as React from 'react'
-import {
-  ErrorComponent,
-  ErrorComponentProps,
-  Link,
-  createFileRoute,
-} from '@tanstack/react-router'
-
-export const Route = createFileRoute('/(test)/foo')({
-  loader: async ({ params: { postId } }) => ({
-    postId,
-    title: 'title',
-    body: 'body',
-    id: 'id',
-  }),
-  errorComponent: PostErrorComponent as any,
-  notFoundComponent: () => {
-    return <p>Post not found</p>
-  },
-  component: PostComponent,
-})
-
-export function PostErrorComponent({ error }: ErrorComponentProps) {
-  return <ErrorComponent error={error} />
-}
-
-function PostComponent() {
-  const post = Route.useLoaderData()
-
-  return (
-    <div className="space-y-2">
-      <h4 className="text-xl font-bold underline">{post.title}</h4>
-      <div className="text-sm">{post.body}</div>
-      <Link
-        to="/posts/$postId/deep"
-        params={{
-          postId: post.id,
-        }}
-        activeProps={{ className: 'text-black font-bold' }}
-        className="block py-1 text-blue-800 hover:text-blue-600"
-      >
-        Deep View
-      </Link>
-    </div>
-  )
-}