diff --git a/e2e/react-router/basepath-file-based/src/routeTree.gen.ts b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts index 59499d9fbf3..c2b36e56082 100644 --- a/e2e/react-router/basepath-file-based/src/routeTree.gen.ts +++ b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts @@ -9,9 +9,15 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as RedirectRouteImport } from './routes/redirect' import { Route as AboutRouteImport } from './routes/about' import { Route as IndexRouteImport } from './routes/index' +const RedirectRoute = RedirectRouteImport.update({ + id: '/redirect', + path: '/redirect', + getParentRoute: () => rootRouteImport, +} as any) const AboutRoute = AboutRouteImport.update({ id: '/about', path: '/about', @@ -26,31 +32,42 @@ const IndexRoute = IndexRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/about': typeof AboutRoute + '/redirect': typeof RedirectRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/about': typeof AboutRoute + '/redirect': typeof RedirectRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/about': typeof AboutRoute + '/redirect': typeof RedirectRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/about' + fullPaths: '/' | '/about' | '/redirect' fileRoutesByTo: FileRoutesByTo - to: '/' | '/about' - id: '__root__' | '/' | '/about' + to: '/' | '/about' | '/redirect' + id: '__root__' | '/' | '/about' | '/redirect' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute AboutRoute: typeof AboutRoute + RedirectRoute: typeof RedirectRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/redirect': { + id: '/redirect' + path: '/redirect' + fullPath: '/redirect' + preLoaderRoute: typeof RedirectRouteImport + parentRoute: typeof rootRouteImport + } '/about': { id: '/about' path: '/about' @@ -71,6 +88,7 @@ declare module '@tanstack/react-router' { const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AboutRoute: AboutRoute, + RedirectRoute: RedirectRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/e2e/react-router/basepath-file-based/src/routes/index.tsx b/e2e/react-router/basepath-file-based/src/routes/index.tsx index 9975e63698d..9a25196630e 100644 --- a/e2e/react-router/basepath-file-based/src/routes/index.tsx +++ b/e2e/react-router/basepath-file-based/src/routes/index.tsx @@ -20,6 +20,16 @@ function App() { > Navigate to /about with document reload + ) } diff --git a/e2e/react-router/basepath-file-based/src/routes/redirect.tsx b/e2e/react-router/basepath-file-based/src/routes/redirect.tsx new file mode 100644 index 00000000000..c3be9254654 --- /dev/null +++ b/e2e/react-router/basepath-file-based/src/routes/redirect.tsx @@ -0,0 +1,12 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect')({ + beforeLoad: async () => { + throw redirect({ to: '/about' }) + }, + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/redirect"!
+} diff --git a/e2e/react-router/basepath-file-based/tests/reload-document.test.ts b/e2e/react-router/basepath-file-based/tests/reload-document.test.ts index 3e60f3bedcb..1baf6807851 100644 --- a/e2e/react-router/basepath-file-based/tests/reload-document.test.ts +++ b/e2e/react-router/basepath-file-based/tests/reload-document.test.ts @@ -16,3 +16,13 @@ test('navigate() respects basepath for when reloadDocument=true', async ({ await page.waitForURL('/app/') await expect(page.getByTestId(`home-component`)).toBeInViewport() }) + +test('redirect respects basepath', async ({ page }) => { + await page.goto(`/app/`) + await expect(page.getByTestId(`home-component`)).toBeInViewport() + + const redirectBtn = page.getByTestId(`to-redirect-btn`) + await redirectBtn.click() + await page.waitForURL('/app/about') + await expect(page.getByTestId(`about-component`)).toBeInViewport() +}) diff --git a/e2e/react-start/custom-basepath/tests/navigation.spec.ts b/e2e/react-start/custom-basepath/tests/navigation.spec.ts index e66f14be9e2..e54ec6dbfdd 100644 --- a/e2e/react-start/custom-basepath/tests/navigation.spec.ts +++ b/e2e/react-start/custom-basepath/tests/navigation.spec.ts @@ -47,9 +47,8 @@ test('Server function URLs correctly include app basepath', async ({ test('client-side redirect', async ({ page, baseURL }) => { await page.goto('/redirect') await page.getByTestId('link-to-throw-it').click() - await page.waitForLoadState('networkidle') - - expect(await page.getByTestId('post-view').isVisible()).toBe(true) + await page.waitForURL(`${baseURL}/posts/1`) + await expect(page.getByTestId('post-view')).toBeInViewport() expect(page.url()).toBe(`${baseURL}/posts/1`) }) diff --git a/packages/router-core/src/rewrite.ts b/packages/router-core/src/rewrite.ts index 0da50c5b620..8eb3f400f44 100644 --- a/packages/router-core/src/rewrite.ts +++ b/packages/router-core/src/rewrite.ts @@ -36,23 +36,37 @@ export function rewriteBasepath(opts: { ? normalizedBasepathWithSlash : normalizedBasepathWithSlash.toLowerCase() + const removeBasePath = (pathname: string) => { + const normalizedPath = opts.caseSensitive + ? pathname + : pathname.toLowerCase() + + // Handle exact basepath match (e.g., /my-app -> /) + if (normalizedPath === checkBasepath) { + return '/' + } + + if (normalizedPath.startsWith(checkBasepathWithSlash)) { + // Handle basepath with trailing content (e.g., /my-app/users -> /users) + return pathname.slice(normalizedBasepath.length) + } + + return pathname + } + return { input: ({ url }) => { - const pathname = opts.caseSensitive - ? url.pathname - : url.pathname.toLowerCase() + url.pathname = removeBasePath(url.pathname) - // 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 }) => { - url.pathname = joinPaths(['/', trimmedBasepath, url.pathname]) + url.pathname = joinPaths([ + '/', + trimmedBasepath, + removeBasePath(url.pathname), + ]) + return url }, } satisfies LocationRewrite diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 54da83a33ec..f5aa2118c52 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1758,11 +1758,11 @@ export class RouterCore< // If a rewrite function is provided, use it to rewrite the URL const rewrittenUrl = executeRewriteOutput(this.rewrite, url) + const rewrittenFullPath = `${rewrittenUrl.pathname}${rewrittenUrl.search}${rewrittenUrl.hash}` return { - publicHref: - rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash, - href: fullPath, + publicHref: rewrittenFullPath, + href: rewrittenFullPath, url: rewrittenUrl, pathname: nextPathname, search: nextSearch,