From 6ff9169501e0354e66972240b434e989eabecaa0 Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Fri, 27 Sep 2024 18:46:40 +0200 Subject: [PATCH] feat: allow throwing external redirects --- .../basic-file-based/package.json | 1 + .../basic-file-based/src/routeTree.gen.ts | 149 +++++++++- .../basic-file-based/src/routes/__root.tsx | 8 + .../src/routes/redirect/$target.tsx | 11 + .../src/routes/redirect/$target/index.tsx | 26 ++ .../redirect/$target/via-beforeLoad.tsx | 13 + .../routes/redirect/$target/via-loader.tsx | 13 + .../src/routes/redirect/index.tsx | 28 ++ .../basic-file-based/tests/redirect.spec.ts | 33 +++ .../basic-file-based/tsconfig.json | 12 +- .../basic/app/components/RedirectOnClick.tsx | 11 + .../basic/app/components/throwRedirect.ts | 14 + e2e/start/basic/app/routeTree.gen.ts | 274 ++++++++++++++++-- e2e/start/basic/app/routes/__root.tsx | 8 + e2e/start/basic/app/routes/redirect.tsx | 9 - .../basic/app/routes/redirect/$target.tsx | 11 + .../app/routes/redirect/$target/index.tsx | 35 +++ .../redirect/$target/serverFn/index.tsx | 35 +++ .../$target/serverFn/via-beforeLoad.tsx | 9 + .../redirect/$target/serverFn/via-loader.tsx | 7 + .../$target/serverFn/via-useServerFn.tsx | 11 + .../redirect/$target/via-beforeLoad.tsx | 13 + .../routes/redirect/$target/via-loader.tsx | 13 + e2e/start/basic/app/routes/redirect/index.tsx | 28 ++ e2e/start/basic/package.json | 1 + e2e/start/basic/tests/redirect.spec.ts | 54 ++++ packages/react-router/src/RouterProvider.tsx | 2 +- packages/react-router/src/index.tsx | 2 +- packages/react-router/src/link.tsx | 2 + packages/react-router/src/redirects.ts | 11 +- packages/react-router/src/route.ts | 2 +- packages/react-router/src/router.ts | 44 ++- packages/react-router/tests/redirect.test.tsx | 2 + packages/react-router/tests/route.test-d.tsx | 20 +- packages/start/src/client/useServerFn.ts | 12 +- packages/start/src/server-handler/index.tsx | 3 +- pnpm-lock.yaml | 6 + 37 files changed, 850 insertions(+), 83 deletions(-) create mode 100644 e2e/react-router/basic-file-based/src/routes/redirect/$target.tsx create mode 100644 e2e/react-router/basic-file-based/src/routes/redirect/$target/index.tsx create mode 100644 e2e/react-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx create mode 100644 e2e/react-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx create mode 100644 e2e/react-router/basic-file-based/src/routes/redirect/index.tsx create mode 100644 e2e/react-router/basic-file-based/tests/redirect.spec.ts create mode 100644 e2e/start/basic/app/components/RedirectOnClick.tsx create mode 100644 e2e/start/basic/app/components/throwRedirect.ts delete mode 100644 e2e/start/basic/app/routes/redirect.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target/index.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target/serverFn/index.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target/via-beforeLoad.tsx create mode 100644 e2e/start/basic/app/routes/redirect/$target/via-loader.tsx create mode 100644 e2e/start/basic/app/routes/redirect/index.tsx create mode 100644 e2e/start/basic/tests/redirect.spec.ts diff --git a/e2e/react-router/basic-file-based/package.json b/e2e/react-router/basic-file-based/package.json index 76f565d77f..c014fdbe4a 100644 --- a/e2e/react-router/basic-file-based/package.json +++ b/e2e/react-router/basic-file-based/package.json @@ -23,6 +23,7 @@ "@types/react": "^18.2.47", "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.3.1", + "combinate": "^1.1.11", "vite": "^5.4.5" } } diff --git a/e2e/react-router/basic-file-based/src/routeTree.gen.ts b/e2e/react-router/basic-file-based/src/routeTree.gen.ts index 149df203a2..a47b9aaa65 100644 --- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts +++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts @@ -14,9 +14,14 @@ import { Route as rootRoute } from './routes/__root' import { Route as PostsImport } from './routes/posts' import { Route as LayoutImport } from './routes/_layout' import { Route as IndexImport } from './routes/index' +import { Route as RedirectIndexImport } from './routes/redirect/index' import { Route as PostsIndexImport } from './routes/posts.index' +import { Route as RedirectTargetImport } from './routes/redirect/$target' import { Route as PostsPostIdImport } from './routes/posts.$postId' import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as RedirectTargetIndexImport } from './routes/redirect/$target/index' +import { Route as RedirectTargetViaLoaderImport } from './routes/redirect/$target/via-loader' +import { Route as RedirectTargetViaBeforeLoadImport } from './routes/redirect/$target/via-beforeLoad' import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b' import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a' @@ -37,11 +42,21 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const RedirectIndexRoute = RedirectIndexImport.update({ + path: '/redirect/', + getParentRoute: () => rootRoute, +} as any) + const PostsIndexRoute = PostsIndexImport.update({ path: '/', getParentRoute: () => PostsRoute, } as any) +const RedirectTargetRoute = RedirectTargetImport.update({ + path: '/redirect/$target', + getParentRoute: () => rootRoute, +} as any) + const PostsPostIdRoute = PostsPostIdImport.update({ path: '/$postId', getParentRoute: () => PostsRoute, @@ -52,6 +67,22 @@ const LayoutLayout2Route = LayoutLayout2Import.update({ getParentRoute: () => LayoutRoute, } as any) +const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({ + path: '/', + getParentRoute: () => RedirectTargetRoute, +} as any) + +const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderImport.update({ + path: '/via-loader', + getParentRoute: () => RedirectTargetRoute, +} as any) + +const RedirectTargetViaBeforeLoadRoute = + RedirectTargetViaBeforeLoadImport.update({ + path: '/via-beforeLoad', + getParentRoute: () => RedirectTargetRoute, + } as any) + const LayoutLayout2LayoutBRoute = LayoutLayout2LayoutBImport.update({ path: '/layout-b', getParentRoute: () => LayoutLayout2Route, @@ -101,6 +132,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsPostIdImport parentRoute: typeof PostsImport } + '/redirect/$target': { + id: '/redirect/$target' + path: '/redirect/$target' + fullPath: '/redirect/$target' + preLoaderRoute: typeof RedirectTargetImport + parentRoute: typeof rootRoute + } '/posts/': { id: '/posts/' path: '/' @@ -108,6 +146,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsIndexImport parentRoute: typeof PostsImport } + '/redirect/': { + id: '/redirect/' + path: '/redirect' + fullPath: '/redirect' + preLoaderRoute: typeof RedirectIndexImport + parentRoute: typeof rootRoute + } '/_layout/_layout-2/layout-a': { id: '/_layout/_layout-2/layout-a' path: '/layout-a' @@ -122,6 +167,27 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutLayout2LayoutBImport parentRoute: typeof LayoutLayout2Import } + '/redirect/$target/via-beforeLoad': { + id: '/redirect/$target/via-beforeLoad' + path: '/via-beforeLoad' + fullPath: '/redirect/$target/via-beforeLoad' + preLoaderRoute: typeof RedirectTargetViaBeforeLoadImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/via-loader': { + id: '/redirect/$target/via-loader' + path: '/via-loader' + fullPath: '/redirect/$target/via-loader' + preLoaderRoute: typeof RedirectTargetViaLoaderImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/': { + id: '/redirect/$target/' + path: '/' + fullPath: '/redirect/$target/' + preLoaderRoute: typeof RedirectTargetIndexImport + parentRoute: typeof RedirectTargetImport + } } } @@ -164,14 +230,35 @@ const PostsRouteChildren: PostsRouteChildren = { const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren) +interface RedirectTargetRouteChildren { + RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute + RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute + RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute +} + +const RedirectTargetRouteChildren: RedirectTargetRouteChildren = { + RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute, + RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute, + RedirectTargetIndexRoute: RedirectTargetIndexRoute, +} + +const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren( + RedirectTargetRouteChildren, +) + export interface FileRoutesByFullPath { '/': typeof IndexRoute '': typeof LayoutLayout2RouteWithChildren '/posts': typeof PostsRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren '/posts/': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute '/layout-a': typeof LayoutLayout2LayoutARoute '/layout-b': typeof LayoutLayout2LayoutBRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute } export interface FileRoutesByTo { @@ -179,8 +266,12 @@ export interface FileRoutesByTo { '': typeof LayoutLayout2RouteWithChildren '/posts/$postId': typeof PostsPostIdRoute '/posts': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute '/layout-a': typeof LayoutLayout2LayoutARoute '/layout-b': typeof LayoutLayout2LayoutBRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target': typeof RedirectTargetIndexRoute } export interface FileRoutesById { @@ -190,9 +281,14 @@ export interface FileRoutesById { '/posts': typeof PostsRouteWithChildren '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren '/posts/': typeof PostsIndexRoute + '/redirect/': typeof RedirectIndexRoute '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute } export interface FileRouteTypes { @@ -202,11 +298,26 @@ export interface FileRouteTypes { | '' | '/posts' | '/posts/$postId' + | '/redirect/$target' | '/posts/' + | '/redirect' | '/layout-a' | '/layout-b' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target/' fileRoutesByTo: FileRoutesByTo - to: '/' | '' | '/posts/$postId' | '/posts' | '/layout-a' | '/layout-b' + to: + | '/' + | '' + | '/posts/$postId' + | '/posts' + | '/redirect' + | '/layout-a' + | '/layout-b' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target' id: | '__root__' | '/' @@ -214,9 +325,14 @@ export interface FileRouteTypes { | '/posts' | '/_layout/_layout-2' | '/posts/$postId' + | '/redirect/$target' | '/posts/' + | '/redirect/' | '/_layout/_layout-2/layout-a' | '/_layout/_layout-2/layout-b' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target/' fileRoutesById: FileRoutesById } @@ -224,12 +340,16 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute LayoutRoute: typeof LayoutRouteWithChildren PostsRoute: typeof PostsRouteWithChildren + RedirectTargetRoute: typeof RedirectTargetRouteWithChildren + RedirectIndexRoute: typeof RedirectIndexRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LayoutRoute: LayoutRouteWithChildren, PostsRoute: PostsRouteWithChildren, + RedirectTargetRoute: RedirectTargetRouteWithChildren, + RedirectIndexRoute: RedirectIndexRoute, } export const routeTree = rootRoute @@ -246,7 +366,9 @@ export const routeTree = rootRoute "children": [ "/", "/_layout", - "/posts" + "/posts", + "/redirect/$target", + "/redirect/" ] }, "/": { @@ -277,10 +399,21 @@ export const routeTree = rootRoute "filePath": "posts.$postId.tsx", "parent": "/posts" }, + "/redirect/$target": { + "filePath": "redirect/$target.tsx", + "children": [ + "/redirect/$target/via-beforeLoad", + "/redirect/$target/via-loader", + "/redirect/$target/" + ] + }, "/posts/": { "filePath": "posts.index.tsx", "parent": "/posts" }, + "/redirect/": { + "filePath": "redirect/index.tsx" + }, "/_layout/_layout-2/layout-a": { "filePath": "_layout/_layout-2/layout-a.tsx", "parent": "/_layout/_layout-2" @@ -288,6 +421,18 @@ export const routeTree = rootRoute "/_layout/_layout-2/layout-b": { "filePath": "_layout/_layout-2/layout-b.tsx", "parent": "/_layout/_layout-2" + }, + "/redirect/$target/via-beforeLoad": { + "filePath": "redirect/$target/via-beforeLoad.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/via-loader": { + "filePath": "redirect/$target/via-loader.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/": { + "filePath": "redirect/$target/index.tsx", + "parent": "/redirect/$target" } } } diff --git a/e2e/react-router/basic-file-based/src/routes/__root.tsx b/e2e/react-router/basic-file-based/src/routes/__root.tsx index 6b57d1e239..4a3db96a55 100644 --- a/e2e/react-router/basic-file-based/src/routes/__root.tsx +++ b/e2e/react-router/basic-file-based/src/routes/__root.tsx @@ -43,6 +43,14 @@ function RootComponent() { > Layout {' '} + + redirect + {' '} ({ target: params.target }), + }, +}) diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/$target/index.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/$target/index.tsx new file mode 100644 index 0000000000..b228e9180c --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/redirect/$target/index.tsx @@ -0,0 +1,26 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/$target/')({ + component: () => ( +
+ + via-beforeLoad + {' '} + + via-loader + {' '} +
+ ), +}) diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx new file mode 100644 index 0000000000..a6cdcddc56 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-beforeLoad.tsx @@ -0,0 +1,13 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({ + beforeLoad: ({ params: { target } }) => { + if (target === 'internal') { + throw redirect({ to: '/posts' }) + } + throw redirect({ + href: 'http://example.com', + }) + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx new file mode 100644 index 0000000000..3430fb76dd --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/redirect/$target/via-loader.tsx @@ -0,0 +1,13 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/$target/via-loader')({ + loader: ({ params: { target } }) => { + if (target === 'internal') { + throw redirect({ to: '/posts' }) + } + throw redirect({ + href: 'http://example.com', + }) + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/react-router/basic-file-based/src/routes/redirect/index.tsx b/e2e/react-router/basic-file-based/src/routes/redirect/index.tsx new file mode 100644 index 0000000000..c0b26a1df4 --- /dev/null +++ b/e2e/react-router/basic-file-based/src/routes/redirect/index.tsx @@ -0,0 +1,28 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/')({ + component: () => ( +
+ + internal + {' '} + + external + +
+ ), +}) diff --git a/e2e/react-router/basic-file-based/tests/redirect.spec.ts b/e2e/react-router/basic-file-based/tests/redirect.spec.ts new file mode 100644 index 0000000000..6ec73d3b76 --- /dev/null +++ b/e2e/react-router/basic-file-based/tests/redirect.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' +import combinateImport from 'combinate' + +// somehow puppeteer does not correctly import default exports +const combinate = (combinateImport as any).default as typeof combinateImport + +test.describe('redirects', () => { + const testMatrix = combinate({ + scenario: ['navigate', 'direct_visit'] as const, + target: ['internal', 'external'] as const, + thrower: ['beforeLoad', 'loader'] as const, + }) + + testMatrix.forEach(({ scenario, target, thrower }) => { + test(`scenario: ${scenario}, target: ${target}, thrower: ${thrower}`, async ({ + page, + }) => { + if (scenario === 'navigate') { + await page.goto(`/redirect/${target}`) + await page.getByRole('link', { name: `via-${thrower}` }).click() + } else { + await page.goto(`/redirect/${target}/via-${thrower}`) + } + + const url = + target === 'internal' + ? 'http://localhost:3001/posts' + : 'http://example.com/' + await page.waitForURL(url) + expect(page.url()).toBe(url) + }) + }) +}) diff --git a/e2e/react-router/basic-file-based/tsconfig.json b/e2e/react-router/basic-file-based/tsconfig.json index c9e17e2b68..ead7d84972 100644 --- a/e2e/react-router/basic-file-based/tsconfig.json +++ b/e2e/react-router/basic-file-based/tsconfig.json @@ -2,6 +2,16 @@ "compilerOptions": { "strict": true, "esModuleInterop": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true } } diff --git a/e2e/start/basic/app/components/RedirectOnClick.tsx b/e2e/start/basic/app/components/RedirectOnClick.tsx new file mode 100644 index 0000000000..76e3308bfa --- /dev/null +++ b/e2e/start/basic/app/components/RedirectOnClick.tsx @@ -0,0 +1,11 @@ +import { useServerFn } from '@tanstack/start' +import { throwRedirect } from './throwRedirect' + +interface RedirectOnClickProps { + target: 'internal' | 'external' +} + +export function RedirectOnClick({ target }: RedirectOnClickProps) { + const execute = useServerFn(throwRedirect) + return +} diff --git a/e2e/start/basic/app/components/throwRedirect.ts b/e2e/start/basic/app/components/throwRedirect.ts new file mode 100644 index 0000000000..f2cde84b2a --- /dev/null +++ b/e2e/start/basic/app/components/throwRedirect.ts @@ -0,0 +1,14 @@ +import { redirect } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/start' + +export const throwRedirect = createServerFn( + 'GET', + (target: 'internal' | 'external') => { + if (target === 'internal') { + throw redirect({ to: '/posts' }) + } + throw redirect({ + href: 'http://example.com', + }) + }, +) diff --git a/e2e/start/basic/app/routeTree.gen.ts b/e2e/start/basic/app/routeTree.gen.ts index cebc80abed..3496d01065 100644 --- a/e2e/start/basic/app/routeTree.gen.ts +++ b/e2e/start/basic/app/routeTree.gen.ts @@ -13,19 +13,27 @@ import { Route as rootRoute } from './routes/__root' import { Route as UsersImport } from './routes/users' import { Route as SearchParamsImport } from './routes/search-params' -import { Route as RedirectImport } from './routes/redirect' import { Route as PostsImport } from './routes/posts' import { Route as DeferredImport } from './routes/deferred' import { Route as LayoutImport } from './routes/_layout' import { Route as IndexImport } from './routes/index' import { Route as UsersIndexImport } from './routes/users.index' +import { Route as RedirectIndexImport } from './routes/redirect/index' import { Route as PostsIndexImport } from './routes/posts.index' import { Route as UsersUserIdImport } from './routes/users.$userId' +import { Route as RedirectTargetImport } from './routes/redirect/$target' import { Route as PostsPostIdImport } from './routes/posts.$postId' import { Route as LayoutLayout2Import } from './routes/_layout/_layout-2' +import { Route as RedirectTargetIndexImport } from './routes/redirect/$target/index' +import { Route as RedirectTargetViaLoaderImport } from './routes/redirect/$target/via-loader' +import { Route as RedirectTargetViaBeforeLoadImport } from './routes/redirect/$target/via-beforeLoad' import { Route as PostsPostIdDeepImport } from './routes/posts_.$postId.deep' import { Route as LayoutLayout2LayoutBImport } from './routes/_layout/_layout-2/layout-b' import { Route as LayoutLayout2LayoutAImport } from './routes/_layout/_layout-2/layout-a' +import { Route as RedirectTargetServerFnIndexImport } from './routes/redirect/$target/serverFn/index' +import { Route as RedirectTargetServerFnViaUseServerFnImport } from './routes/redirect/$target/serverFn/via-useServerFn' +import { Route as RedirectTargetServerFnViaLoaderImport } from './routes/redirect/$target/serverFn/via-loader' +import { Route as RedirectTargetServerFnViaBeforeLoadImport } from './routes/redirect/$target/serverFn/via-beforeLoad' // Create/Update Routes @@ -39,11 +47,6 @@ const SearchParamsRoute = SearchParamsImport.update({ getParentRoute: () => rootRoute, } as any) -const RedirectRoute = RedirectImport.update({ - path: '/redirect', - getParentRoute: () => rootRoute, -} as any) - const PostsRoute = PostsImport.update({ path: '/posts', getParentRoute: () => rootRoute, @@ -69,6 +72,11 @@ const UsersIndexRoute = UsersIndexImport.update({ getParentRoute: () => UsersRoute, } as any) +const RedirectIndexRoute = RedirectIndexImport.update({ + path: '/redirect/', + getParentRoute: () => rootRoute, +} as any) + const PostsIndexRoute = PostsIndexImport.update({ path: '/', getParentRoute: () => PostsRoute, @@ -79,6 +87,11 @@ const UsersUserIdRoute = UsersUserIdImport.update({ getParentRoute: () => UsersRoute, } as any) +const RedirectTargetRoute = RedirectTargetImport.update({ + path: '/redirect/$target', + getParentRoute: () => rootRoute, +} as any) + const PostsPostIdRoute = PostsPostIdImport.update({ path: '/$postId', getParentRoute: () => PostsRoute, @@ -89,6 +102,22 @@ const LayoutLayout2Route = LayoutLayout2Import.update({ getParentRoute: () => LayoutRoute, } as any) +const RedirectTargetIndexRoute = RedirectTargetIndexImport.update({ + path: '/', + getParentRoute: () => RedirectTargetRoute, +} as any) + +const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderImport.update({ + path: '/via-loader', + getParentRoute: () => RedirectTargetRoute, +} as any) + +const RedirectTargetViaBeforeLoadRoute = + RedirectTargetViaBeforeLoadImport.update({ + path: '/via-beforeLoad', + getParentRoute: () => RedirectTargetRoute, + } as any) + const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({ path: '/posts/$postId/deep', getParentRoute: () => rootRoute, @@ -104,6 +133,30 @@ const LayoutLayout2LayoutARoute = LayoutLayout2LayoutAImport.update({ getParentRoute: () => LayoutLayout2Route, } as any) +const RedirectTargetServerFnIndexRoute = + RedirectTargetServerFnIndexImport.update({ + path: '/serverFn/', + getParentRoute: () => RedirectTargetRoute, + } as any) + +const RedirectTargetServerFnViaUseServerFnRoute = + RedirectTargetServerFnViaUseServerFnImport.update({ + path: '/serverFn/via-useServerFn', + getParentRoute: () => RedirectTargetRoute, + } as any) + +const RedirectTargetServerFnViaLoaderRoute = + RedirectTargetServerFnViaLoaderImport.update({ + path: '/serverFn/via-loader', + getParentRoute: () => RedirectTargetRoute, + } as any) + +const RedirectTargetServerFnViaBeforeLoadRoute = + RedirectTargetServerFnViaBeforeLoadImport.update({ + path: '/serverFn/via-beforeLoad', + getParentRoute: () => RedirectTargetRoute, + } as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -136,13 +189,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsImport parentRoute: typeof rootRoute } - '/redirect': { - id: '/redirect' - path: '/redirect' - fullPath: '/redirect' - preLoaderRoute: typeof RedirectImport - parentRoute: typeof rootRoute - } '/search-params': { id: '/search-params' path: '/search-params' @@ -171,6 +217,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsPostIdImport parentRoute: typeof PostsImport } + '/redirect/$target': { + id: '/redirect/$target' + path: '/redirect/$target' + fullPath: '/redirect/$target' + preLoaderRoute: typeof RedirectTargetImport + parentRoute: typeof rootRoute + } '/users/$userId': { id: '/users/$userId' path: '/$userId' @@ -185,6 +238,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsIndexImport parentRoute: typeof PostsImport } + '/redirect/': { + id: '/redirect/' + path: '/redirect' + fullPath: '/redirect' + preLoaderRoute: typeof RedirectIndexImport + parentRoute: typeof rootRoute + } '/users/': { id: '/users/' path: '/' @@ -213,6 +273,55 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsPostIdDeepImport parentRoute: typeof rootRoute } + '/redirect/$target/via-beforeLoad': { + id: '/redirect/$target/via-beforeLoad' + path: '/via-beforeLoad' + fullPath: '/redirect/$target/via-beforeLoad' + preLoaderRoute: typeof RedirectTargetViaBeforeLoadImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/via-loader': { + id: '/redirect/$target/via-loader' + path: '/via-loader' + fullPath: '/redirect/$target/via-loader' + preLoaderRoute: typeof RedirectTargetViaLoaderImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/': { + id: '/redirect/$target/' + path: '/' + fullPath: '/redirect/$target/' + preLoaderRoute: typeof RedirectTargetIndexImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/via-beforeLoad': { + id: '/redirect/$target/serverFn/via-beforeLoad' + path: '/serverFn/via-beforeLoad' + fullPath: '/redirect/$target/serverFn/via-beforeLoad' + preLoaderRoute: typeof RedirectTargetServerFnViaBeforeLoadImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/via-loader': { + id: '/redirect/$target/serverFn/via-loader' + path: '/serverFn/via-loader' + fullPath: '/redirect/$target/serverFn/via-loader' + preLoaderRoute: typeof RedirectTargetServerFnViaLoaderImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/via-useServerFn': { + id: '/redirect/$target/serverFn/via-useServerFn' + path: '/serverFn/via-useServerFn' + fullPath: '/redirect/$target/serverFn/via-useServerFn' + preLoaderRoute: typeof RedirectTargetServerFnViaUseServerFnImport + parentRoute: typeof RedirectTargetImport + } + '/redirect/$target/serverFn/': { + id: '/redirect/$target/serverFn/' + path: '/serverFn' + fullPath: '/redirect/$target/serverFn' + preLoaderRoute: typeof RedirectTargetServerFnIndexImport + parentRoute: typeof RedirectTargetImport + } } } @@ -267,36 +376,77 @@ const UsersRouteChildren: UsersRouteChildren = { const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren) +interface RedirectTargetRouteChildren { + RedirectTargetViaBeforeLoadRoute: typeof RedirectTargetViaBeforeLoadRoute + RedirectTargetViaLoaderRoute: typeof RedirectTargetViaLoaderRoute + RedirectTargetIndexRoute: typeof RedirectTargetIndexRoute + RedirectTargetServerFnViaBeforeLoadRoute: typeof RedirectTargetServerFnViaBeforeLoadRoute + RedirectTargetServerFnViaLoaderRoute: typeof RedirectTargetServerFnViaLoaderRoute + RedirectTargetServerFnViaUseServerFnRoute: typeof RedirectTargetServerFnViaUseServerFnRoute + RedirectTargetServerFnIndexRoute: typeof RedirectTargetServerFnIndexRoute +} + +const RedirectTargetRouteChildren: RedirectTargetRouteChildren = { + RedirectTargetViaBeforeLoadRoute: RedirectTargetViaBeforeLoadRoute, + RedirectTargetViaLoaderRoute: RedirectTargetViaLoaderRoute, + RedirectTargetIndexRoute: RedirectTargetIndexRoute, + RedirectTargetServerFnViaBeforeLoadRoute: + RedirectTargetServerFnViaBeforeLoadRoute, + RedirectTargetServerFnViaLoaderRoute: RedirectTargetServerFnViaLoaderRoute, + RedirectTargetServerFnViaUseServerFnRoute: + RedirectTargetServerFnViaUseServerFnRoute, + RedirectTargetServerFnIndexRoute: RedirectTargetServerFnIndexRoute, +} + +const RedirectTargetRouteWithChildren = RedirectTargetRoute._addFileChildren( + RedirectTargetRouteChildren, +) + export interface FileRoutesByFullPath { '/': typeof IndexRoute '': typeof LayoutLayout2RouteWithChildren '/deferred': typeof DeferredRoute '/posts': typeof PostsRouteWithChildren - '/redirect': typeof RedirectRoute '/search-params': typeof SearchParamsRoute '/users': typeof UsersRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren '/users/$userId': typeof UsersUserIdRoute '/posts/': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute '/users/': typeof UsersIndexRoute '/layout-a': typeof LayoutLayout2LayoutARoute '/layout-b': typeof LayoutLayout2LayoutBRoute '/posts/$postId/deep': typeof PostsPostIdDeepRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute + '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute + '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute + '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute + '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '': typeof LayoutLayout2RouteWithChildren '/deferred': typeof DeferredRoute - '/redirect': typeof RedirectRoute '/search-params': typeof SearchParamsRoute '/posts/$postId': typeof PostsPostIdRoute '/users/$userId': typeof UsersUserIdRoute '/posts': typeof PostsIndexRoute + '/redirect': typeof RedirectIndexRoute '/users': typeof UsersIndexRoute '/layout-a': typeof LayoutLayout2LayoutARoute '/layout-b': typeof LayoutLayout2LayoutBRoute '/posts/$postId/deep': typeof PostsPostIdDeepRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target': typeof RedirectTargetIndexRoute + '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute + '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute + '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute + '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute } export interface FileRoutesById { @@ -305,17 +455,25 @@ export interface FileRoutesById { '/_layout': typeof LayoutRouteWithChildren '/deferred': typeof DeferredRoute '/posts': typeof PostsRouteWithChildren - '/redirect': typeof RedirectRoute '/search-params': typeof SearchParamsRoute '/users': typeof UsersRouteWithChildren '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren '/posts/$postId': typeof PostsPostIdRoute + '/redirect/$target': typeof RedirectTargetRouteWithChildren '/users/$userId': typeof UsersUserIdRoute '/posts/': typeof PostsIndexRoute + '/redirect/': typeof RedirectIndexRoute '/users/': typeof UsersIndexRoute '/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute '/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute '/posts/$postId/deep': typeof PostsPostIdDeepRoute + '/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute + '/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute + '/redirect/$target/': typeof RedirectTargetIndexRoute + '/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute + '/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute + '/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute + '/redirect/$target/serverFn/': typeof RedirectTargetServerFnIndexRoute } export interface FileRouteTypes { @@ -325,47 +483,70 @@ export interface FileRouteTypes { | '' | '/deferred' | '/posts' - | '/redirect' | '/search-params' | '/users' | '/posts/$postId' + | '/redirect/$target' | '/users/$userId' | '/posts/' + | '/redirect' | '/users/' | '/layout-a' | '/layout-b' | '/posts/$postId/deep' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target/' + | '/redirect/$target/serverFn/via-beforeLoad' + | '/redirect/$target/serverFn/via-loader' + | '/redirect/$target/serverFn/via-useServerFn' + | '/redirect/$target/serverFn' fileRoutesByTo: FileRoutesByTo to: | '/' | '' | '/deferred' - | '/redirect' | '/search-params' | '/posts/$postId' | '/users/$userId' | '/posts' + | '/redirect' | '/users' | '/layout-a' | '/layout-b' | '/posts/$postId/deep' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target' + | '/redirect/$target/serverFn/via-beforeLoad' + | '/redirect/$target/serverFn/via-loader' + | '/redirect/$target/serverFn/via-useServerFn' + | '/redirect/$target/serverFn' id: | '__root__' | '/' | '/_layout' | '/deferred' | '/posts' - | '/redirect' | '/search-params' | '/users' | '/_layout/_layout-2' | '/posts/$postId' + | '/redirect/$target' | '/users/$userId' | '/posts/' + | '/redirect/' | '/users/' | '/_layout/_layout-2/layout-a' | '/_layout/_layout-2/layout-b' | '/posts/$postId/deep' + | '/redirect/$target/via-beforeLoad' + | '/redirect/$target/via-loader' + | '/redirect/$target/' + | '/redirect/$target/serverFn/via-beforeLoad' + | '/redirect/$target/serverFn/via-loader' + | '/redirect/$target/serverFn/via-useServerFn' + | '/redirect/$target/serverFn/' fileRoutesById: FileRoutesById } @@ -374,9 +555,10 @@ export interface RootRouteChildren { LayoutRoute: typeof LayoutRouteWithChildren DeferredRoute: typeof DeferredRoute PostsRoute: typeof PostsRouteWithChildren - RedirectRoute: typeof RedirectRoute SearchParamsRoute: typeof SearchParamsRoute UsersRoute: typeof UsersRouteWithChildren + RedirectTargetRoute: typeof RedirectTargetRouteWithChildren + RedirectIndexRoute: typeof RedirectIndexRoute PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute } @@ -385,9 +567,10 @@ const rootRouteChildren: RootRouteChildren = { LayoutRoute: LayoutRouteWithChildren, DeferredRoute: DeferredRoute, PostsRoute: PostsRouteWithChildren, - RedirectRoute: RedirectRoute, SearchParamsRoute: SearchParamsRoute, UsersRoute: UsersRouteWithChildren, + RedirectTargetRoute: RedirectTargetRouteWithChildren, + RedirectIndexRoute: RedirectIndexRoute, PostsPostIdDeepRoute: PostsPostIdDeepRoute, } @@ -407,9 +590,10 @@ export const routeTree = rootRoute "/_layout", "/deferred", "/posts", - "/redirect", "/search-params", "/users", + "/redirect/$target", + "/redirect/", "/posts/$postId/deep" ] }, @@ -432,9 +616,6 @@ export const routeTree = rootRoute "/posts/" ] }, - "/redirect": { - "filePath": "redirect.tsx" - }, "/search-params": { "filePath": "search-params.tsx" }, @@ -457,6 +638,18 @@ export const routeTree = rootRoute "filePath": "posts.$postId.tsx", "parent": "/posts" }, + "/redirect/$target": { + "filePath": "redirect/$target.tsx", + "children": [ + "/redirect/$target/via-beforeLoad", + "/redirect/$target/via-loader", + "/redirect/$target/", + "/redirect/$target/serverFn/via-beforeLoad", + "/redirect/$target/serverFn/via-loader", + "/redirect/$target/serverFn/via-useServerFn", + "/redirect/$target/serverFn/" + ] + }, "/users/$userId": { "filePath": "users.$userId.tsx", "parent": "/users" @@ -465,6 +658,9 @@ export const routeTree = rootRoute "filePath": "posts.index.tsx", "parent": "/posts" }, + "/redirect/": { + "filePath": "redirect/index.tsx" + }, "/users/": { "filePath": "users.index.tsx", "parent": "/users" @@ -479,6 +675,34 @@ export const routeTree = rootRoute }, "/posts/$postId/deep": { "filePath": "posts_.$postId.deep.tsx" + }, + "/redirect/$target/via-beforeLoad": { + "filePath": "redirect/$target/via-beforeLoad.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/via-loader": { + "filePath": "redirect/$target/via-loader.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/": { + "filePath": "redirect/$target/index.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-beforeLoad": { + "filePath": "redirect/$target/serverFn/via-beforeLoad.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-loader": { + "filePath": "redirect/$target/serverFn/via-loader.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/via-useServerFn": { + "filePath": "redirect/$target/serverFn/via-useServerFn.tsx", + "parent": "/redirect/$target" + }, + "/redirect/$target/serverFn/": { + "filePath": "redirect/$target/serverFn/index.tsx", + "parent": "/redirect/$target" } } } diff --git a/e2e/start/basic/app/routes/__root.tsx b/e2e/start/basic/app/routes/__root.tsx index 4ef88cfb27..6dca980e27 100644 --- a/e2e/start/basic/app/routes/__root.tsx +++ b/e2e/start/basic/app/routes/__root.tsx @@ -118,6 +118,14 @@ function RootDocument({ children }: { children: React.ReactNode }) { > Deferred {' '} + + redirect + {' '} { - throw redirect({ - to: '/posts', - }) - }, -}) diff --git a/e2e/start/basic/app/routes/redirect/$target.tsx b/e2e/start/basic/app/routes/redirect/$target.tsx new file mode 100644 index 0000000000..1a489a9921 --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' +import { z } from 'zod' + +export const Route = createFileRoute('/redirect/$target')({ + params: { + parse: z.object({ + target: z.union([z.literal('internal'), z.literal('external')]), + }).parse, + stringify: (params) => ({ target: params.target }), + }, +}) diff --git a/e2e/start/basic/app/routes/redirect/$target/index.tsx b/e2e/start/basic/app/routes/redirect/$target/index.tsx new file mode 100644 index 0000000000..1d8d2faf72 --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target/index.tsx @@ -0,0 +1,35 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/$target/')({ + component: () => ( +
+ + via-beforeLoad + {' '} + + via-loader + {' '} + + serverFn + +
+ ), +}) diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/index.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/index.tsx new file mode 100644 index 0000000000..6609e2191a --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/index.tsx @@ -0,0 +1,35 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/$target/serverFn/')({ + component: () => ( +
+ + via-beforeLoad + {' '} + + via-loader + {' '} + + via-useServerFn + +
+ ), +}) diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx new file mode 100644 index 0000000000..9db20d2f42 --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-beforeLoad.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' +import { throwRedirect } from '~/components/throwRedirect' + +export const Route = createFileRoute( + '/redirect/$target/serverFn/via-beforeLoad', +)({ + beforeLoad: ({ params: { target } }) => throwRedirect(target), + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx new file mode 100644 index 0000000000..ddf6dad0cd --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-loader.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router' +import { throwRedirect } from '~/components/throwRedirect' + +export const Route = createFileRoute('/redirect/$target/serverFn/via-loader')({ + loader: ({ params: { target } }) => throwRedirect(target), + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx new file mode 100644 index 0000000000..86c384c5f0 --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target/serverFn/via-useServerFn.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' +import { RedirectOnClick } from '~/components/RedirectOnClick' + +export const Route = createFileRoute( + '/redirect/$target/serverFn/via-useServerFn', +)({ + component: () => { + const { target } = Route.useParams() + return + }, +}) diff --git a/e2e/start/basic/app/routes/redirect/$target/via-beforeLoad.tsx b/e2e/start/basic/app/routes/redirect/$target/via-beforeLoad.tsx new file mode 100644 index 0000000000..a6cdcddc56 --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target/via-beforeLoad.tsx @@ -0,0 +1,13 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/$target/via-beforeLoad')({ + beforeLoad: ({ params: { target } }) => { + if (target === 'internal') { + throw redirect({ to: '/posts' }) + } + throw redirect({ + href: 'http://example.com', + }) + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/start/basic/app/routes/redirect/$target/via-loader.tsx b/e2e/start/basic/app/routes/redirect/$target/via-loader.tsx new file mode 100644 index 0000000000..3430fb76dd --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/$target/via-loader.tsx @@ -0,0 +1,13 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/$target/via-loader')({ + loader: ({ params: { target } }) => { + if (target === 'internal') { + throw redirect({ to: '/posts' }) + } + throw redirect({ + href: 'http://example.com', + }) + }, + component: () =>
{Route.fullPath}
, +}) diff --git a/e2e/start/basic/app/routes/redirect/index.tsx b/e2e/start/basic/app/routes/redirect/index.tsx new file mode 100644 index 0000000000..c0b26a1df4 --- /dev/null +++ b/e2e/start/basic/app/routes/redirect/index.tsx @@ -0,0 +1,28 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/redirect/')({ + component: () => ( +
+ + internal + {' '} + + external + +
+ ), +}) diff --git a/e2e/start/basic/package.json b/e2e/start/basic/package.json index ee73a02d5f..98b41349d8 100644 --- a/e2e/start/basic/package.json +++ b/e2e/start/basic/package.json @@ -27,6 +27,7 @@ "@types/react-dom": "^18.2.21", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", + "combinate": "^1.1.11", "postcss": "^8.4.47", "tailwindcss": "^3.4.11", "typescript": "^5.6.2", diff --git a/e2e/start/basic/tests/redirect.spec.ts b/e2e/start/basic/tests/redirect.spec.ts new file mode 100644 index 0000000000..943470279c --- /dev/null +++ b/e2e/start/basic/tests/redirect.spec.ts @@ -0,0 +1,54 @@ +import { expect, test } from '@playwright/test' +import combinateImport from 'combinate' + +// somehow puppeteer does not correctly import default exports +const combinate = (combinateImport as any).default as typeof combinateImport + +test.describe('redirects', () => { + const testMatrix = combinate({ + scenario: ['navigate', 'direct_visit'] as const, + target: ['internal', 'external'] as const, + thrower: ['beforeLoad', 'loader'] as const, + }) + + testMatrix.forEach(({ scenario, target, thrower }) => { + test(`scenario: ${scenario}, target: ${target}, thrower: ${thrower}`, async ({ + page, + }) => { + if (scenario === 'navigate') { + await page.goto(`/redirect/${target}`) + await page.getByRole('link', { name: `via-${thrower}` }).click() + } else { + await page.goto(`/redirect/${target}/via-${thrower}`) + } + + const url = + target === 'internal' + ? 'http://localhost:3000/posts' + : 'http://example.com/' + await page.waitForURL(url) + expect(page.url()).toBe(url) + }) + }) + + const serverFnTestMatrix = combinate({ + target: ['internal', 'external'] as const, + thrower: ['beforeLoad', 'loader', 'useServerFn'] as const, + }) + + serverFnTestMatrix.forEach(({ target, thrower }) => { + test(`serverFn redirects to target: ${target}, thrower: ${thrower}`, async ({ + page, + }) => { + await page.goto(`/redirect/${target}/serverFn/via-${thrower}`) + await page.getByRole('button', { name: 'click me', exact: true }).click() + + const url = + target === 'internal' + ? 'http://localhost:3000/posts' + : 'http://example.com/' + await page.waitForURL(url) + expect(page.url()).toBe(url) + }) + }) +}) diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx index a2b8fddafe..9630d06413 100644 --- a/packages/react-router/src/RouterProvider.tsx +++ b/packages/react-router/src/RouterProvider.tsx @@ -37,7 +37,7 @@ export type NavigateFn = < TMaskTo extends string = '', >( opts: NavigateOptions, -) => Promise +) => Promise | void export type BuildLocationFn = < TRouter extends RegisteredRouter, diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx index 1560645c90..ffe62198d2 100644 --- a/packages/react-router/src/index.tsx +++ b/packages/react-router/src/index.tsx @@ -126,7 +126,7 @@ export type { Segment } from './path' export { encode, decode } from './qss' -export { redirect, isRedirect } from './redirects' +export { redirect, isRedirect, isResolvedRedirect } from './redirects' export type { AnyRedirect, Redirect, ResolvedRedirect } from './redirects' export { rootRouteId } from './root' diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index 2c728e2b94..16898fd83a 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -212,6 +212,8 @@ export interface NavigateOptionProps { // if set to `true`, the router will wrap the resulting navigation in a document.startViewTransition() call. viewTransition?: boolean ignoreBlocker?: boolean + _type?: 'internal' | 'external' + href?: string } export type ToOptions< diff --git a/packages/react-router/src/redirects.ts b/packages/react-router/src/redirects.ts index b1a29a6893..29f379571d 100644 --- a/packages/react-router/src/redirects.ts +++ b/packages/react-router/src/redirects.ts @@ -12,14 +12,12 @@ export type Redirect< TMaskFrom extends RoutePaths | string = TFrom, TMaskTo extends string = '.', > = { - /** - * @deprecated Use `statusCode` instead - **/ href?: string code?: number statusCode?: number throw?: any headers?: HeadersInit + _type?: 'internal' | 'external' } & NavigateOptions export type ResolvedRedirect< @@ -47,6 +45,13 @@ export function redirect< ;(opts as any).isRedirect = true opts.statusCode = opts.statusCode || opts.code || 307 opts.headers = opts.headers || {} + + opts._type = 'internal' + try { + new URL(`${opts.href}`) + opts._type = 'external' + } catch {} + if (opts.throw) { throw opts } diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts index 156494b2e3..5b20db3f42 100644 --- a/packages/react-router/src/route.ts +++ b/packages/react-router/src/route.ts @@ -547,7 +547,7 @@ export interface LoaderFnContext< /** * @deprecated Use `throw redirect({ to: '/somewhere' })` instead **/ - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts index 5044c95694..baa390f22f 100644 --- a/packages/react-router/src/router.ts +++ b/packages/react-router/src/router.ts @@ -493,6 +493,7 @@ export interface BuildNextOptions { } from?: string _fromLocation?: ParsedLocation + href?: string } export interface DehydratedRouterState { @@ -1606,9 +1607,9 @@ export class Router< resetScroll, viewTransition, ignoreBlocker, + href, ...rest }: BuildNextOptions & CommitLocationOptions = {}) => { - const href = (rest as any).href if (href) { const parsed = parseHref(href, {}) rest.to = parsed.pathname @@ -1626,29 +1627,19 @@ export class Router< }) } - navigate: NavigateFn = ({ to, ...rest }) => { - // If this link simply reloads the current route, - // make sure it has a new key so it will trigger a data refresh - - // If this `to` is a valid external URL, return - // null for LinkUtils - const toString = String(to) - let isExternal - - try { - new URL(`${toString}`) - isExternal = true - } catch (e) {} - - invariant( - !isExternal, - 'Attempting to navigate to external url with router.navigate!', - ) - + navigate: NavigateFn = ({ to, _type, href, ...rest }) => { + if (href && _type === 'external') { + if (rest.replace) { + window.location.replace(href) + } else { + window.location.href = href + } + return + } return this.buildAndCommitLocation({ ...rest, + href, to: to as string, - // to: toString, }) } @@ -1779,7 +1770,7 @@ export class Router< redirect = err if (!this.isServer) { this.navigate({ - ...err, + ...redirect, replace: true, ignoreBlocker: true, }) @@ -1907,7 +1898,11 @@ export class Router< } const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => { - if (isResolvedRedirect(err)) throw err + if (isResolvedRedirect(err)) { + if (err._type === 'internal') { + throw err + } + } if (isRedirect(err) || isNotFound(err)) { updateMatch(match.id, (prev) => ({ @@ -2529,6 +2524,9 @@ export class Router< return matches } catch (err) { if (isRedirect(err)) { + if (err._type === 'external') { + return undefined + } return await this.preloadRoute({ ...(err as any), _fromLocation: next, diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx index b9b252a504..1fc5020f23 100644 --- a/packages/react-router/tests/redirect.test.tsx +++ b/packages/react-router/tests/redirect.test.tsx @@ -207,6 +207,7 @@ describe('redirect', () => { search: {}, searchStr: '', }), + _type: 'internal', to: '/about', headers: {}, href: '/about', @@ -259,6 +260,7 @@ describe('redirect', () => { searchStr: '', }), to: '/about', + _type: 'internal', headers: {}, href: '/about', isRedirect: true, diff --git a/packages/react-router/tests/route.test-d.tsx b/packages/react-router/tests/route.test-d.tsx index 9becb50205..6d2de73fdb 100644 --- a/packages/react-router/tests/route.test-d.tsx +++ b/packages/react-router/tests/route.test-d.tsx @@ -82,7 +82,7 @@ test('when creating the root with a loader', () => { deps: {} context: {} location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -177,7 +177,7 @@ test('when creating the root route with context and a loader', () => { deps: {} context: { userId: string } location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -245,7 +245,7 @@ test('when creating the root route with context, routeContext, beforeLoad and a deps: {} context: { userId: string; permission: 'view'; env: 'env1' } location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -373,7 +373,7 @@ test('when creating a child route with a loader from the root route', () => { deps: {} context: {} location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -415,7 +415,7 @@ test('when creating a child route with a loader from the root route with context deps: {} context: { userId: string } location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -548,7 +548,7 @@ test('when creating a child route with params, search and loader from the root r deps: {} context: {} location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -573,7 +573,7 @@ test('when creating a child route with params, search, loader and loaderDeps fro deps: { page: number } context: {} location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -597,7 +597,7 @@ test('when creating a child route with params, search, loader and loaderDeps fro deps: { page: number } context: { userId: string } location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -700,7 +700,7 @@ test('when creating a child route with params, search with routeContext, beforeL deps: {} context: { userId: string; env: string; readonly permission: 'view' } location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route @@ -1008,7 +1008,7 @@ test('when creating a child route with routeContext, beforeLoad, search, params, detailsPermissions: readonly ['view'] } location: ParsedLocation - navigate: (opts: NavigateOptions) => Promise + navigate: (opts: NavigateOptions) => Promise | void parentMatchPromise?: Promise cause: 'preload' | 'enter' | 'stay' route: Route diff --git a/packages/start/src/client/useServerFn.ts b/packages/start/src/client/useServerFn.ts index 1bdfa4bc5e..241e102daf 100644 --- a/packages/start/src/client/useServerFn.ts +++ b/packages/start/src/client/useServerFn.ts @@ -16,12 +16,12 @@ export function useServerFn) => Promise>( return res } catch (err) { if (isRedirect(err)) { - router.navigate( - router.resolveRedirect({ - ...err, - _fromLocation: router.state.location, - }), - ) + const resolvedRedirect = router.resolveRedirect({ + ...err, + _fromLocation: router.state.location, + }) + + return router.navigate(resolvedRedirect) } throw err diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 955f2738ce..e533cc1eb7 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -3,6 +3,7 @@ import { defaultParseSearch, isNotFound, isRedirect, + isResolvedRedirect, } from '@tanstack/react-router' import invariant from 'tiny-invariant' import { eventHandler, toWebRequest } from 'vinxi/http' @@ -181,7 +182,7 @@ function redirectOrNotFoundResponse(error: any) { headers: { 'Content-Type': 'application/json', [serverFnReturnTypeHeader]: 'json', - ...(error.headers || {}), + ...(headers || {}), }, }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b37bc842c..a4118e8d5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,6 +179,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.1 version: 4.3.1(vite@5.4.5(@types/node@22.5.4)(terser@5.31.1)) + combinate: + specifier: ^1.1.11 + version: 1.1.11 vite: specifier: ^5.4.5 version: 5.4.5(@types/node@22.5.4)(terser@5.31.1) @@ -397,6 +400,9 @@ importers: autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.47) + combinate: + specifier: ^1.1.11 + version: 1.1.11 postcss: specifier: ^8.4.47 version: 8.4.47