diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx index d58dfe2126c..2214e9001eb 100644 --- a/packages/react-router/tests/router.test.tsx +++ b/packages/react-router/tests/router.test.tsx @@ -2670,6 +2670,144 @@ describe('rewriteBasepath utility', () => { expect(router.state.location.pathname).toBe('/users') }) + it.each([ + { + description: 'basepath with leading slash but without trailing slash', + basepath: '/api/v1', + }, + { + description: 'basepath without leading slash but with trailing slash', + basepath: 'api/v1/', + }, + { + description: 'basepath without leading and trailing slashes', + basepath: 'api/v1', + }, + ])('should handle $description', async ({ basepath }) => { + const rootRoute = createRootRoute({ + component: () => , + }) + + const usersRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/users', + component: () =>
Users
, + }) + + const routeTree = rootRoute.addChildren([usersRoute]) + + const router = createRouter({ + routeTree, + history: createMemoryHistory({ + initialEntries: ['/api/v1/users'], + }), + rewrite: rewriteBasepath({ basepath }), + }) + + render() + + await waitFor(() => { + expect(screen.getByTestId('users')).toBeInTheDocument() + }) + + expect(router.state.location.pathname).toBe('/users') + }) + + it.each([ + { description: 'has trailing slash', basepath: '/my-app/' }, + { description: 'has no trailing slash', basepath: '/my-app' }, + ])( + 'should not resolve to 404 when basepath $description and URL matches', + async ({ basepath }) => { + const rootRoute = createRootRoute({ + component: () => , + }) + + const homeRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () =>
Home
, + }) + + const usersRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/users', + component: () =>
Users
, + }) + + const routeTree = rootRoute.addChildren([homeRoute, usersRoute]) + + const router = createRouter({ + routeTree, + history: createMemoryHistory({ + initialEntries: ['/my-app/'], + }), + rewrite: rewriteBasepath({ basepath }), + }) + + render() + + await waitFor(() => { + expect(screen.getByTestId('home')).toBeInTheDocument() + }) + + expect(router.state.location.pathname).toBe('/') + expect(router.state.statusCode).toBe(200) + }, + ) + + it.each([ + { description: 'with trailing slash', basepath: '/my-app/' }, + { description: 'without trailing slash', basepath: '/my-app' }, + ])( + 'should handle basepath $description when navigating to root path', + async ({ basepath }) => { + const rootRoute = createRootRoute({ + component: () => , + }) + + const homeRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', + component: () => ( +
+ + About + +
+ ), + }) + + const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/about', + component: () =>
About
, + }) + + const routeTree = rootRoute.addChildren([homeRoute, aboutRoute]) + + const history = createMemoryHistory({ initialEntries: ['/my-app/'] }) + + const router = createRouter({ + routeTree, + history, + rewrite: rewriteBasepath({ basepath }), + }) + + render() + + const aboutLink = await screen.findByTestId('about-link') + fireEvent.click(aboutLink) + + await waitFor(() => { + expect(screen.getByTestId('about')).toBeInTheDocument() + }) + + expect(router.state.location.pathname).toBe('/about') + expect(history.location.pathname).toBe('/my-app/about') + }, + ) + it('should handle empty basepath gracefully', async () => { const rootRoute = createRootRoute({ component: () => , diff --git a/packages/router-core/src/rewrite.ts b/packages/router-core/src/rewrite.ts index c0cef0cc879..7ffb49b8e15 100644 --- a/packages/router-core/src/rewrite.ts +++ b/packages/router-core/src/rewrite.ts @@ -23,13 +23,28 @@ export function rewriteBasepath(opts: { caseSensitive?: boolean }) { const trimmedBasepath = trimPath(opts.basepath) - const regex = new RegExp( - `^/${trimmedBasepath}/`, - opts.caseSensitive ? '' : 'i', - ) + const normalizedBasepath = `/${trimmedBasepath}` + const normalizedBasepathWithSlash = `${normalizedBasepath}/` + const checkBasepath = opts.caseSensitive + ? normalizedBasepath + : normalizedBasepath.toLowerCase() + const checkBasepathWithSlash = opts.caseSensitive + ? normalizedBasepathWithSlash + : normalizedBasepathWithSlash.toLowerCase() + return { input: ({ url }) => { - url.pathname = url.pathname.replace(regex, '/') + const pathname = opts.caseSensitive + ? url.pathname + : url.pathname.toLowerCase() + + // Handle exact basepath match (e.g., /my-app -> /) + if (pathname === checkBasepath) { + url.pathname = '/' + } else if (pathname.startsWith(checkBasepathWithSlash)) { + // Handle basepath with trailing content (e.g., /my-app/users -> /users) + url.pathname = url.pathname.slice(normalizedBasepath.length) + } return url }, output: ({ url }) => {