Skip to content

Commit bb410f8

Browse files
fix(router): rewriteBasepath not working without trailing slash (#5244)
Co-authored-by: Manuel Schiller <[email protected]>
1 parent ae5119d commit bb410f8

File tree

2 files changed

+158
-5
lines changed

2 files changed

+158
-5
lines changed

packages/react-router/tests/router.test.tsx

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2670,6 +2670,144 @@ describe('rewriteBasepath utility', () => {
26702670
expect(router.state.location.pathname).toBe('/users')
26712671
})
26722672

2673+
it.each([
2674+
{
2675+
description: 'basepath with leading slash but without trailing slash',
2676+
basepath: '/api/v1',
2677+
},
2678+
{
2679+
description: 'basepath without leading slash but with trailing slash',
2680+
basepath: 'api/v1/',
2681+
},
2682+
{
2683+
description: 'basepath without leading and trailing slashes',
2684+
basepath: 'api/v1',
2685+
},
2686+
])('should handle $description', async ({ basepath }) => {
2687+
const rootRoute = createRootRoute({
2688+
component: () => <Outlet />,
2689+
})
2690+
2691+
const usersRoute = createRoute({
2692+
getParentRoute: () => rootRoute,
2693+
path: '/users',
2694+
component: () => <div data-testid="users">Users</div>,
2695+
})
2696+
2697+
const routeTree = rootRoute.addChildren([usersRoute])
2698+
2699+
const router = createRouter({
2700+
routeTree,
2701+
history: createMemoryHistory({
2702+
initialEntries: ['/api/v1/users'],
2703+
}),
2704+
rewrite: rewriteBasepath({ basepath }),
2705+
})
2706+
2707+
render(<RouterProvider router={router} />)
2708+
2709+
await waitFor(() => {
2710+
expect(screen.getByTestId('users')).toBeInTheDocument()
2711+
})
2712+
2713+
expect(router.state.location.pathname).toBe('/users')
2714+
})
2715+
2716+
it.each([
2717+
{ description: 'has trailing slash', basepath: '/my-app/' },
2718+
{ description: 'has no trailing slash', basepath: '/my-app' },
2719+
])(
2720+
'should not resolve to 404 when basepath $description and URL matches',
2721+
async ({ basepath }) => {
2722+
const rootRoute = createRootRoute({
2723+
component: () => <Outlet />,
2724+
})
2725+
2726+
const homeRoute = createRoute({
2727+
getParentRoute: () => rootRoute,
2728+
path: '/',
2729+
component: () => <div data-testid="home">Home</div>,
2730+
})
2731+
2732+
const usersRoute = createRoute({
2733+
getParentRoute: () => rootRoute,
2734+
path: '/users',
2735+
component: () => <div data-testid="users">Users</div>,
2736+
})
2737+
2738+
const routeTree = rootRoute.addChildren([homeRoute, usersRoute])
2739+
2740+
const router = createRouter({
2741+
routeTree,
2742+
history: createMemoryHistory({
2743+
initialEntries: ['/my-app/'],
2744+
}),
2745+
rewrite: rewriteBasepath({ basepath }),
2746+
})
2747+
2748+
render(<RouterProvider router={router} />)
2749+
2750+
await waitFor(() => {
2751+
expect(screen.getByTestId('home')).toBeInTheDocument()
2752+
})
2753+
2754+
expect(router.state.location.pathname).toBe('/')
2755+
expect(router.state.statusCode).toBe(200)
2756+
},
2757+
)
2758+
2759+
it.each([
2760+
{ description: 'with trailing slash', basepath: '/my-app/' },
2761+
{ description: 'without trailing slash', basepath: '/my-app' },
2762+
])(
2763+
'should handle basepath $description when navigating to root path',
2764+
async ({ basepath }) => {
2765+
const rootRoute = createRootRoute({
2766+
component: () => <Outlet />,
2767+
})
2768+
2769+
const homeRoute = createRoute({
2770+
getParentRoute: () => rootRoute,
2771+
path: '/',
2772+
component: () => (
2773+
<div>
2774+
<Link to="/about" data-testid="about-link">
2775+
About
2776+
</Link>
2777+
</div>
2778+
),
2779+
})
2780+
2781+
const aboutRoute = createRoute({
2782+
getParentRoute: () => rootRoute,
2783+
path: '/about',
2784+
component: () => <div data-testid="about">About</div>,
2785+
})
2786+
2787+
const routeTree = rootRoute.addChildren([homeRoute, aboutRoute])
2788+
2789+
const history = createMemoryHistory({ initialEntries: ['/my-app/'] })
2790+
2791+
const router = createRouter({
2792+
routeTree,
2793+
history,
2794+
rewrite: rewriteBasepath({ basepath }),
2795+
})
2796+
2797+
render(<RouterProvider router={router} />)
2798+
2799+
const aboutLink = await screen.findByTestId('about-link')
2800+
fireEvent.click(aboutLink)
2801+
2802+
await waitFor(() => {
2803+
expect(screen.getByTestId('about')).toBeInTheDocument()
2804+
})
2805+
2806+
expect(router.state.location.pathname).toBe('/about')
2807+
expect(history.location.pathname).toBe('/my-app/about')
2808+
},
2809+
)
2810+
26732811
it('should handle empty basepath gracefully', async () => {
26742812
const rootRoute = createRootRoute({
26752813
component: () => <Outlet />,

packages/router-core/src/rewrite.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,28 @@ export function rewriteBasepath(opts: {
2323
caseSensitive?: boolean
2424
}) {
2525
const trimmedBasepath = trimPath(opts.basepath)
26-
const regex = new RegExp(
27-
`^/${trimmedBasepath}/`,
28-
opts.caseSensitive ? '' : 'i',
29-
)
26+
const normalizedBasepath = `/${trimmedBasepath}`
27+
const normalizedBasepathWithSlash = `${normalizedBasepath}/`
28+
const checkBasepath = opts.caseSensitive
29+
? normalizedBasepath
30+
: normalizedBasepath.toLowerCase()
31+
const checkBasepathWithSlash = opts.caseSensitive
32+
? normalizedBasepathWithSlash
33+
: normalizedBasepathWithSlash.toLowerCase()
34+
3035
return {
3136
input: ({ url }) => {
32-
url.pathname = url.pathname.replace(regex, '/')
37+
const pathname = opts.caseSensitive
38+
? url.pathname
39+
: url.pathname.toLowerCase()
40+
41+
// Handle exact basepath match (e.g., /my-app -> /)
42+
if (pathname === checkBasepath) {
43+
url.pathname = '/'
44+
} else if (pathname.startsWith(checkBasepathWithSlash)) {
45+
// Handle basepath with trailing content (e.g., /my-app/users -> /users)
46+
url.pathname = url.pathname.slice(normalizedBasepath.length)
47+
}
3348
return url
3449
},
3550
output: ({ url }) => {

0 commit comments

Comments
 (0)