Skip to content

Commit ee96896

Browse files
committed
fix(router): rewriteBasepath not working without trailing slash
1 parent 7771c00 commit ee96896

File tree

2 files changed

+276
-5
lines changed

2 files changed

+276
-5
lines changed

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

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

2673+
it('should handle basepath with leading slash but without trailing slash', async () => {
2674+
const rootRoute = createRootRoute({
2675+
component: () => <Outlet />,
2676+
})
2677+
2678+
const usersRoute = createRoute({
2679+
getParentRoute: () => rootRoute,
2680+
path: '/users',
2681+
component: () => <div data-testid="users">Users</div>,
2682+
})
2683+
2684+
const routeTree = rootRoute.addChildren([usersRoute])
2685+
2686+
const router = createRouter({
2687+
routeTree,
2688+
history: createMemoryHistory({
2689+
initialEntries: ['/api/v1/users'],
2690+
}),
2691+
rewrite: rewriteBasepath({ basepath: '/api/v1' }), // With leading slash but no trailing slash
2692+
})
2693+
2694+
render(<RouterProvider router={router} />)
2695+
2696+
await waitFor(() => {
2697+
expect(screen.getByTestId('users')).toBeInTheDocument()
2698+
})
2699+
2700+
expect(router.state.location.pathname).toBe('/users')
2701+
})
2702+
2703+
it('should handle basepath without leading slash but with trailing slash', async () => {
2704+
const rootRoute = createRootRoute({
2705+
component: () => <Outlet />,
2706+
})
2707+
2708+
const usersRoute = createRoute({
2709+
getParentRoute: () => rootRoute,
2710+
path: '/users',
2711+
component: () => <div data-testid="users">Users</div>,
2712+
})
2713+
2714+
const routeTree = rootRoute.addChildren([usersRoute])
2715+
2716+
const router = createRouter({
2717+
routeTree,
2718+
history: createMemoryHistory({
2719+
initialEntries: ['/api/v1/users'],
2720+
}),
2721+
rewrite: rewriteBasepath({ basepath: 'api/v1/' }), // Without leading slash but with trailing slash
2722+
})
2723+
2724+
render(<RouterProvider router={router} />)
2725+
2726+
await waitFor(() => {
2727+
expect(screen.getByTestId('users')).toBeInTheDocument()
2728+
})
2729+
2730+
expect(router.state.location.pathname).toBe('/users')
2731+
})
2732+
2733+
it('should handle basepath without leading and trailing slashes', async () => {
2734+
const rootRoute = createRootRoute({
2735+
component: () => <Outlet />,
2736+
})
2737+
2738+
const usersRoute = createRoute({
2739+
getParentRoute: () => rootRoute,
2740+
path: '/users',
2741+
component: () => <div data-testid="users">Users</div>,
2742+
})
2743+
2744+
const routeTree = rootRoute.addChildren([usersRoute])
2745+
2746+
const router = createRouter({
2747+
routeTree,
2748+
history: createMemoryHistory({
2749+
initialEntries: ['/api/v1/users'],
2750+
}),
2751+
rewrite: rewriteBasepath({ basepath: 'api/v1' }), // Without leading and trailing slashes
2752+
})
2753+
2754+
render(<RouterProvider router={router} />)
2755+
2756+
await waitFor(() => {
2757+
expect(screen.getByTestId('users')).toBeInTheDocument()
2758+
})
2759+
2760+
expect(router.state.location.pathname).toBe('/users')
2761+
})
2762+
2763+
it('should not resolve to 404 when basepath has trailing slash and URL matches', async () => {
2764+
const rootRoute = createRootRoute({
2765+
component: () => <Outlet />,
2766+
})
2767+
2768+
const homeRoute = createRoute({
2769+
getParentRoute: () => rootRoute,
2770+
path: '/',
2771+
component: () => <div data-testid="home">Home</div>,
2772+
})
2773+
2774+
const usersRoute = createRoute({
2775+
getParentRoute: () => rootRoute,
2776+
path: '/users',
2777+
component: () => <div data-testid="users">Users</div>,
2778+
})
2779+
2780+
const routeTree = rootRoute.addChildren([homeRoute, usersRoute])
2781+
2782+
const router = createRouter({
2783+
routeTree,
2784+
history: createMemoryHistory({
2785+
initialEntries: ['/my-app/'],
2786+
}),
2787+
rewrite: rewriteBasepath({ basepath: '/my-app/' }), // With trailing slash
2788+
})
2789+
2790+
render(<RouterProvider router={router} />)
2791+
2792+
await waitFor(() => {
2793+
expect(screen.getByTestId('home')).toBeInTheDocument()
2794+
})
2795+
2796+
expect(router.state.location.pathname).toBe('/')
2797+
expect(router.state.statusCode).toBe(200)
2798+
})
2799+
2800+
it('should not resolve to 404 when basepath has no trailing slash and URL matches', async () => {
2801+
const rootRoute = createRootRoute({
2802+
component: () => <Outlet />,
2803+
})
2804+
2805+
const homeRoute = createRoute({
2806+
getParentRoute: () => rootRoute,
2807+
path: '/',
2808+
component: () => <div data-testid="home">Home</div>,
2809+
})
2810+
2811+
const usersRoute = createRoute({
2812+
getParentRoute: () => rootRoute,
2813+
path: '/users',
2814+
component: () => <div data-testid="users">Users</div>,
2815+
})
2816+
2817+
const routeTree = rootRoute.addChildren([homeRoute, usersRoute])
2818+
2819+
const router = createRouter({
2820+
routeTree,
2821+
history: createMemoryHistory({
2822+
initialEntries: ['/my-app'],
2823+
}),
2824+
rewrite: rewriteBasepath({ basepath: '/my-app' }), // Without trailing slash
2825+
})
2826+
2827+
render(<RouterProvider router={router} />)
2828+
2829+
await waitFor(() => {
2830+
expect(screen.getByTestId('home')).toBeInTheDocument()
2831+
})
2832+
2833+
expect(router.state.location.pathname).toBe('/')
2834+
expect(router.state.statusCode).toBe(200)
2835+
})
2836+
2837+
it('should handle basepath with trailing slash when navigating to root path', async () => {
2838+
const rootRoute = createRootRoute({
2839+
component: () => <Outlet />,
2840+
})
2841+
2842+
const homeRoute = createRoute({
2843+
getParentRoute: () => rootRoute,
2844+
path: '/',
2845+
component: () => (
2846+
<div>
2847+
<Link to="/about" data-testid="about-link">
2848+
About
2849+
</Link>
2850+
</div>
2851+
),
2852+
})
2853+
2854+
const aboutRoute = createRoute({
2855+
getParentRoute: () => rootRoute,
2856+
path: '/about',
2857+
component: () => <div data-testid="about">About</div>,
2858+
})
2859+
2860+
const routeTree = rootRoute.addChildren([homeRoute, aboutRoute])
2861+
2862+
const history = createMemoryHistory({ initialEntries: ['/my-app/'] })
2863+
2864+
const router = createRouter({
2865+
routeTree,
2866+
history,
2867+
rewrite: rewriteBasepath({ basepath: '/my-app/' }), // With trailing slash
2868+
})
2869+
2870+
render(<RouterProvider router={router} />)
2871+
2872+
const aboutLink = await screen.findByTestId('about-link')
2873+
fireEvent.click(aboutLink)
2874+
2875+
await waitFor(() => {
2876+
expect(screen.getByTestId('about')).toBeInTheDocument()
2877+
})
2878+
2879+
expect(router.state.location.pathname).toBe('/about')
2880+
expect(history.location.pathname).toBe('/my-app/about')
2881+
})
2882+
2883+
it('should handle basepath without trailing slash when navigating to root path', async () => {
2884+
const rootRoute = createRootRoute({
2885+
component: () => <Outlet />,
2886+
})
2887+
2888+
const homeRoute = createRoute({
2889+
getParentRoute: () => rootRoute,
2890+
path: '/',
2891+
component: () => (
2892+
<div>
2893+
<Link to="/about" data-testid="about-link">
2894+
About
2895+
</Link>
2896+
</div>
2897+
),
2898+
})
2899+
2900+
const aboutRoute = createRoute({
2901+
getParentRoute: () => rootRoute,
2902+
path: '/about',
2903+
component: () => <div data-testid="about">About</div>,
2904+
})
2905+
2906+
const routeTree = rootRoute.addChildren([homeRoute, aboutRoute])
2907+
2908+
const history = createMemoryHistory({ initialEntries: ['/my-app'] })
2909+
2910+
const router = createRouter({
2911+
routeTree,
2912+
history,
2913+
rewrite: rewriteBasepath({ basepath: '/my-app' }), // Without trailing slash
2914+
})
2915+
2916+
render(<RouterProvider router={router} />)
2917+
2918+
const aboutLink = await screen.findByTestId('about-link')
2919+
fireEvent.click(aboutLink)
2920+
2921+
await waitFor(() => {
2922+
expect(screen.getByTestId('about')).toBeInTheDocument()
2923+
})
2924+
2925+
expect(router.state.location.pathname).toBe('/about')
2926+
expect(history.location.pathname).toBe('/my-app/about')
2927+
})
2928+
26732929
it('should handle empty basepath gracefully', async () => {
26742930
const rootRoute = createRootRoute({
26752931
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+
3029
return {
3130
input: ({ url }) => {
32-
url.pathname = url.pathname.replace(regex, '/')
31+
const pathname = opts.caseSensitive
32+
? url.pathname
33+
: url.pathname.toLowerCase()
34+
const checkBasepath = opts.caseSensitive
35+
? normalizedBasepath
36+
: normalizedBasepath.toLowerCase()
37+
const checkBasepathWithSlash = opts.caseSensitive
38+
? normalizedBasepathWithSlash
39+
: normalizedBasepathWithSlash.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)