Repository: TanStack/router
Pull request #2270 (2024-09-05 11:18:03)

test(router-generator): add tests to the utils

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

CI is running for commit 4e31a25638e45aee9630b036ef3ffd3fd44902a2.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/66d99386fc8059439b2a236c?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/bef68136-3bba-494d-ac3c-d5a9d0937399)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/e85485ca-b808-48b6-8167-2cebc34afa11)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/1a166cd8-6f6d-40bf-84c5-453e14ad264f)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/7044e639-208c-4db1-9d17-737e16777186)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/8743dfa9-b6c8-468b-8b1a-824106c49505)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/15fda127-cb39-4c1b-9b4b-2a3a79b4599f)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/63907691-64ab-42ef-84d1-ff5c92610fe5)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/5e46c7d5-8ccd-4b04-8c5b-6be4fc8fbe52)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/3bf467d0-6ec5-49b5-9d0d-0481441f9876)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/ccf7b3ce-7083-4a07-9847-01134905a07f)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/883e2bee-a1f0-4f61-9353-8a8ce83168b2)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/896f2155-60de-4072-82ef-92036f09db2f)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/4f41c45c-c564-41cd-9abd-ce6c4e3c07f9)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/b5058986-4755-4802-a4d7-05f2cf288283)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/8fe04d69-9474-453f-8547-ae58262f0db8)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/3f158464-30d9-48f7-b01a-7b077f116513)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/6b3eb172-561d-4519-8a44-7413b9e1ecfd)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/f62604e2-4d11-4d97-bb4b-d883419e7d09)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/cc18f7a1-f890-4fe4-8bf2-a0751768503a)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/37ebbf13-e919-4373-ac7e-a69248cddfb1)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/8da186c3-c813-4642-a150-495869fd991a)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/88094c2e-53ca-4306-85f5-37bb01268aaa)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/6bfae770-07b8-4d69-be75-6625388757b1)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/30c8bee0-ab31-431f-bbca-cb6f1501fc0b)
- [tanstack-router-search-validator-adapters](https://pkg.pr.new/template/1018c92e-949e-4599-b39d-328edad68d4a)
- [tanstack-router-example-react-start-basic](https://pkg.pr.new/template/ce65f811-496e-45b8-b70a-22fdbbc57c7f)
- [tanstack-router-example-react-start-basic-auth](https://pkg.pr.new/template/988d9504-dc2e-4142-85db-69e03bf27533)
- [tanstack-router-example-react-start-basic-counter](https://pkg.pr.new/template/2ae003e8-6688-4a4d-933d-c18a8c7b1d29)
- [tanstack-router-example-react-start-basic-react-query](https://pkg.pr.new/template/a23f29a2-59a7-4810-9c36-a3cbf6dcc9a3)
- [tanstack-router-example-react-start-basic-rsc](https://pkg.pr.new/template/50af4c80-7f20-4af5-bb84-cb9e06b7dd1c)
- [tanstack-router-example-react-start-clerk-basic](https://pkg.pr.new/template/fc5e2f75-a300-4190-b598-cc2e0f425232)
- [tanstack-router-example-react-start-trello-trellaux](https://pkg.pr.new/template/3565d4a0-a4c9-42a5-be01-84c7c52c8a1d)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/c6795787-2a0c-4393-8c59-02db204fc530)
- [tanstack-router-example-react-start-trellaux](https://pkg.pr.new/template/228d58da-6e8c-4eab-b112-7410422cddc9)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/8c0fbb82-4c21-4f4a-96c1-376864f53893)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/ec26e260-9206-47a4-96b7-dd8603a1ba93)

</p></details>
      
    


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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

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

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

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/29723187456"><code>4e31a25</code></a>_


Patch:
diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts
index d91e561a1..f282381db 100644
--- a/packages/router-generator/src/generator.ts
+++ b/packages/router-generator/src/generator.ts
@@ -5,6 +5,7 @@ import * as prettier from 'prettier'
 import {
   determineInitialRoutePath,
   logging,
+  multiSortBy,
   removeExt,
   removeTrailingSlash,
   removeUnderscores,
@@ -671,36 +672,6 @@ function spaces(d: number): string {
     .join('')
 }
 
-export function multiSortBy<T>(
-  arr: Array<T>,
-  accessors: Array<(item: T) => any> = [(d) => d],
-): Array<T> {
-  return arr
-    .map((d, i) => [d, i] as const)
-    .sort(([a, ai], [b, bi]) => {
-      for (const accessor of accessors) {
-        const ao = accessor(a)
-        const bo = accessor(b)
-
-        if (typeof ao === 'undefined') {
-          if (typeof bo === 'undefined') {
-            continue
-          }
-          return 1
-        }
-
-        if (ao === bo) {
-          continue
-        }
-
-        return ao > bo ? 1 : -1
-      }
-
-      return ai - bi
-    })
-    .map(([d]) => d)
-}
-
 function removeTrailingUnderscores(s?: string) {
   return s?.replaceAll(/(_$)/gi, '').replaceAll(/(_\/)/gi, '/')
 }
diff --git a/packages/router-generator/src/utils.ts b/packages/router-generator/src/utils.ts
index ffcac863f..201341f77 100644
--- a/packages/router-generator/src/utils.ts
+++ b/packages/router-generator/src/utils.ts
@@ -1,3 +1,33 @@
+export function multiSortBy<T>(
+  arr: Array<T>,
+  accessors: Array<(item: T) => any> = [(d) => d],
+): Array<T> {
+  return arr
+    .map((d, i) => [d, i] as const)
+    .sort(([a, ai], [b, bi]) => {
+      for (const accessor of accessors) {
+        const ao = accessor(a)
+        const bo = accessor(b)
+
+        if (typeof ao === 'undefined') {
+          if (typeof bo === 'undefined') {
+            continue
+          }
+          return 1
+        }
+
+        if (ao === bo) {
+          continue
+        }
+
+        return ao > bo ? 1 : -1
+      }
+
+      return ai - bi
+    })
+    .map(([d]) => d)
+}
+
 export function cleanPath(path: string) {
   // remove double slashes
   return path.replace(/\/{2,}/g, '/')


Test patch:
diff --git a/packages/router-generator/tests/utils.test.ts b/packages/router-generator/tests/utils.test.ts
new file mode 100644
index 000000000..22cb05f20
--- /dev/null
+++ b/packages/router-generator/tests/utils.test.ts
@@ -0,0 +1,125 @@
+import { describe, expect, it } from 'vitest'
+import {
+  cleanPath,
+  determineInitialRoutePath,
+  multiSortBy,
+  removeExt,
+  removeUnderscores,
+  routePathToVariable,
+} from '../src/utils'
+
+describe('cleanPath', () => {
+  it('keeps path with leading slash and trailing slash', () => {
+    expect(cleanPath('/test/')).toBe('/test/')
+  })
+})
+
+describe('determineInitialRoutePath', () => {
+  it('removes dots and adds slashes', () => {
+    expect(determineInitialRoutePath('test.test')).toBe('/test/test')
+  })
+
+  it('keeps leading slash', () => {
+    expect(determineInitialRoutePath('/test.test')).toBe('/test/test')
+  })
+
+  it('keeps trailing slash', () => {
+    expect(determineInitialRoutePath('test.test/')).toBe('/test/test/')
+  })
+
+  it('removes dots and adds slashes with leading and trailing slashes', () => {
+    expect(determineInitialRoutePath('/test.test/')).toBe('/test/test/')
+  })
+
+  it("returns '/' if path is empty", () => {
+    expect(determineInitialRoutePath('')).toBe('/')
+  })
+
+  it("returns '/' if path is '.'", () => {
+    expect(determineInitialRoutePath('.')).toBe('/')
+  })
+
+  it("returns '/' if path is './'", () => {
+    expect(determineInitialRoutePath('./')).toBe('/')
+  })
+})
+
+describe('multiSortBy', () => {
+  it('sorts by multiple criteria', () => {
+    const data = [
+      { routePath: '/test/1/2/index', f: 'b' },
+      { routePath: '/test/1', f: 'b' },
+      { routePath: '/test/1/2/3/4/index', f: 'b' },
+      { routePath: '/test/1/2/3', f: 'b' },
+      { routePath: '/test/1/2/3/index', f: 'b' },
+      { routePath: '/test/1/2', f: 'b' },
+      { routePath: '/test/1/2/3/4', f: 'b' },
+    ]
+
+    const sorted = multiSortBy(data, [
+      (d) => (d.routePath.includes('1') ? -1 : 1),
+      (d) => d.routePath.split('/').length,
+      (d) => (d.routePath.endsWith('index') ? -1 : 1),
+      (d) => d,
+    ])
+
+    expect(sorted).toEqual([
+      { routePath: '/test/1', f: 'b' },
+      { routePath: '/test/1/2', f: 'b' },
+      { routePath: '/test/1/2/index', f: 'b' },
+      { routePath: '/test/1/2/3', f: 'b' },
+      { routePath: '/test/1/2/3/index', f: 'b' },
+      { routePath: '/test/1/2/3/4', f: 'b' },
+      { routePath: '/test/1/2/3/4/index', f: 'b' },
+    ])
+  })
+})
+
+describe('removeExt', () => {
+  it('removes extension', () => {
+    expect(removeExt('test.ts')).toBe('test')
+  })
+
+  it('does not remove extension if no extension', () => {
+    expect(removeExt('test')).toBe('test')
+  })
+
+  it('removes extension with multiple dots', () => {
+    expect(removeExt('test.test.ts')).toBe('test.test')
+  })
+
+  it('removes extension with leading dot', () => {
+    expect(removeExt('.test.ts')).toBe('.test')
+  })
+
+  it('removes extension when in a route path', () => {
+    expect(removeExt('/test/test.ts')).toBe('/test/test')
+  })
+})
+
+describe('removeUnderscores', () => {
+  it('removes leading underscore', () => {
+    expect(removeUnderscores('_test')).toBe('test')
+  })
+
+  it('removes trailing underscore', () => {
+    expect(removeUnderscores('test_')).toBe('test')
+  })
+
+  it('removes leading and trailing underscores', () => {
+    expect(removeUnderscores('_test_')).toBe('test')
+  })
+})
+
+describe('routePathToVariable', () => {
+  it.each([
+    ['/test/$/index', 'TestSplatIndex'],
+    ['/test/$', 'TestSplat'],
+    ['/test/$/', 'TestSplat'],
+    ['/test/index', 'TestIndex'],
+    ['/test', 'Test'],
+    ['/test/', 'Test'],
+  ])(`converts "%s" to "%s"`, (routePath, expected) => {
+    expect(routePathToVariable(routePath)).toBe(expected)
+  })
+})