Repository: TanStack/router
Pull request #1787 (2024-06-19 10:13:33)

fix: proper basename removal handling
Previous implementation did not cover cases of partial match and cases with basepath matched later within the path.

Reworked version removes basepath only from the beginning of pathname and only in case it fully matches path segment.

Giving the basename is `/app`
| input | old | new |
|------:|:----|:-----|
| `/application` | `lication` | `/application` |
| `/app/project/applications` | `/projectlications` | `/project/applications` |
| `/app/app/app` | `` | `/app/app` |

fix: #1779

schiller-manuel: LGTM!
we need to sort out a CI issue, as soon as this is up and running again we'll merge this

Patch:
diff --git a/packages/react-router/src/path.ts b/packages/react-router/src/path.ts
index 545d581e9..09106d796 100644
--- a/packages/react-router/src/path.ts
+++ b/packages/react-router/src/path.ts
@@ -82,6 +82,7 @@ interface ResolvePathOptions {
   to: string
   trailingSlash?: 'always' | 'never' | 'preserve'
 }
+
 export function resolvePath({
   basepath,
   base,
@@ -196,6 +197,7 @@ interface InterpolatePathOptions {
   leaveWildcards?: boolean
   leaveParams?: boolean
 }
+
 export function interpolatePath({
   path,
   params,
@@ -241,7 +243,36 @@ export function matchPathname(
 }
 
 export function removeBasepath(basepath: string, pathname: string) {
-  return basepath != '/' ? pathname.replace(basepath, '') : pathname
+  switch (true) {
+    // default behaviour is to serve app from the root - pathname
+    // left untouched
+    case basepath === '/':
+      return pathname
+
+    // shortcut for removing the basepath from the equal pathname
+    case pathname === basepath:
+      return ''
+
+    // in case pathname is shorter than basepath - there is
+    // nothing to remove
+    case pathname.length < basepath.length:
+      return pathname
+
+    // avoid matching partial segments - strict equality handled
+    // earlier, otherwise, basepath separated from pathname with
+    // separator, therefore lack of separator means partial
+    // segment match (`/app` should not match `/application`)
+    case pathname[basepath.length] !== '/':
+      return pathname
+
+    // remove the basepath from the pathname in case there is any
+    case pathname.startsWith(basepath):
+      return pathname.slice(basepath.length)
+
+    // otherwise, return the pathname as is
+    default:
+      return pathname
+  }
 }
 
 export function matchByPath(


Test patch:
diff --git a/packages/react-router/tests/path.test.ts b/packages/react-router/tests/path.test.ts
index 894681a5c..9f587606d 100644
--- a/packages/react-router/tests/path.test.ts
+++ b/packages/react-router/tests/path.test.ts
@@ -1,5 +1,48 @@
 import { describe, expect, it } from 'vitest'
-import { exactPathTest, removeTrailingSlash } from '../src/path'
+import { exactPathTest, removeBasepath, removeTrailingSlash } from '../src/path'
+
+describe('removeBasepath', () => {
+  it.each([
+    {
+      name: '`/` should leave pathname as-is',
+      basepath: '/',
+      pathname: '/path',
+      expected: '/path',
+    },
+    {
+      name: 'should return empty string if basepath is the same as pathname',
+      basepath: '/path',
+      pathname: '/path',
+      expected: '',
+    },
+    {
+      name: 'should remove basepath from the beginning of the pathname',
+      basepath: '/app',
+      pathname: '/app/path/app',
+      expected: '/path/app',
+    },
+    {
+      name: 'should remove multisegment basepath from the beginning of the pathname',
+      basepath: '/app/new',
+      pathname: '/app/new/path/app/new',
+      expected: '/path/app/new',
+    },
+    {
+      name: 'should remove basepath only in case it matches segments completely',
+      basepath: '/app',
+      pathname: '/application',
+      expected: '/application',
+    },
+    {
+      name: 'should remove multisegment basepath only in case it matches segments completely',
+      basepath: '/app/new',
+      pathname: '/app/new-application',
+      expected: '/app/new-application',
+    },
+  ])('$name', ({ basepath, pathname, expected }) => {
+    expect(removeBasepath(basepath, pathname)).toBe(expected)
+  })
+})
 
 describe.each([{ basepath: '/' }, { basepath: '/app' }, { basepath: '/app/' }])(
   'removeTrailingSlash with basepath $basepath',