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 28197e4d98..d8f74a9b0f 100644 --- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts +++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts @@ -33,6 +33,7 @@ import { Route as StructuralSharingEnabledRouteImport } from './routes/structura import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default' import { Route as RedirectTargetRouteImport } from './routes/redirect/$target' import { Route as PostsPostIdRouteImport } from './routes/posts.$postId' +import { Route as PipeReferenceRouteImport } from './routes/pipe.$reference' import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2' import { Route as groupLazyinsideRouteImport } from './routes/(group)/lazyinside' import { Route as groupInsideRouteImport } from './routes/(group)/inside' @@ -239,6 +240,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({ path: '/$postId', getParentRoute: () => PostsRoute, } as any) +const PipeReferenceRoute = PipeReferenceRouteImport.update({ + id: '/pipe/$reference', + path: '/pipe/$reference', + getParentRoute: () => rootRouteImport, +} as any) const LayoutLayout2Route = LayoutLayout2RouteImport.update({ id: '/_layout-2', getParentRoute: () => LayoutRoute, @@ -726,6 +732,7 @@ export interface FileRoutesByFullPath { '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute '/inside': typeof groupInsideRoute '/lazyinside': typeof groupLazyinsideRoute + '/pipe/$reference': typeof PipeReferenceRoute '/posts/$postId': typeof PostsPostIdRoute '/redirect/$target': typeof RedirectTargetRouteWithChildren '/search-params/default': typeof SearchParamsDefaultRoute @@ -830,6 +837,7 @@ export interface FileRoutesByTo { '/onlyrouteinside': typeof anotherGroupOnlyrouteinsideRoute '/inside': typeof groupInsideRoute '/lazyinside': typeof groupLazyinsideRoute + '/pipe/$reference': typeof PipeReferenceRoute '/posts/$postId': typeof PostsPostIdRoute '/search-params/default': typeof SearchParamsDefaultRoute '/structural-sharing/$enabled': typeof StructuralSharingEnabledRoute @@ -932,6 +940,7 @@ export interface FileRoutesById { '/(group)/inside': typeof groupInsideRoute '/(group)/lazyinside': typeof groupLazyinsideRoute '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren + '/pipe/$reference': typeof PipeReferenceRoute '/posts/$postId': typeof PostsPostIdRoute '/redirect/$target': typeof RedirectTargetRouteWithChildren '/search-params/default': typeof SearchParamsDefaultRoute @@ -1040,6 +1049,7 @@ export interface FileRouteTypes { | '/onlyrouteinside' | '/inside' | '/lazyinside' + | '/pipe/$reference' | '/posts/$postId' | '/redirect/$target' | '/search-params/default' @@ -1144,6 +1154,7 @@ export interface FileRouteTypes { | '/onlyrouteinside' | '/inside' | '/lazyinside' + | '/pipe/$reference' | '/posts/$postId' | '/search-params/default' | '/structural-sharing/$enabled' @@ -1245,6 +1256,7 @@ export interface FileRouteTypes { | '/(group)/inside' | '/(group)/lazyinside' | '/_layout/_layout-2' + | '/pipe/$reference' | '/posts/$postId' | '/redirect/$target' | '/search-params/default' @@ -1349,6 +1361,7 @@ export interface RootRouteChildren { groupLayoutRoute: typeof groupLayoutRouteWithChildren groupInsideRoute: typeof groupInsideRoute groupLazyinsideRoute: typeof groupLazyinsideRoute + PipeReferenceRoute: typeof PipeReferenceRoute RedirectTargetRoute: typeof RedirectTargetRouteWithChildren StructuralSharingEnabledRoute: typeof StructuralSharingEnabledRoute ParamsPsIndexRoute: typeof ParamsPsIndexRoute @@ -1542,6 +1555,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsPostIdRouteImport parentRoute: typeof PostsRoute } + '/pipe/$reference': { + id: '/pipe/$reference' + path: '/pipe/$reference' + fullPath: '/pipe/$reference' + preLoaderRoute: typeof PipeReferenceRouteImport + parentRoute: typeof rootRouteImport + } '/_layout/_layout-2': { id: '/_layout/_layout-2' path: '' @@ -2605,6 +2625,7 @@ const rootRouteChildren: RootRouteChildren = { groupLayoutRoute: groupLayoutRouteWithChildren, groupInsideRoute: groupInsideRoute, groupLazyinsideRoute: groupLazyinsideRoute, + PipeReferenceRoute: PipeReferenceRoute, RedirectTargetRoute: RedirectTargetRouteWithChildren, StructuralSharingEnabledRoute: StructuralSharingEnabledRoute, ParamsPsIndexRoute: ParamsPsIndexRoute, 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 dde8139a6e..ff04634fcb 100644 --- a/e2e/react-router/basic-file-based/src/routes/__root.tsx +++ b/e2e/react-router/basic-file-based/src/routes/__root.tsx @@ -156,6 +156,15 @@ function RootComponent() { > Masks {' '} + + Dynamic Pipe Character + {' '} Hello {params.reference}! +} diff --git a/e2e/react-router/basic-file-based/tests/app.spec.ts b/e2e/react-router/basic-file-based/tests/app.spec.ts index f644c10042..43f429ad2a 100644 --- a/e2e/react-router/basic-file-based/tests/app.spec.ts +++ b/e2e/react-router/basic-file-based/tests/app.spec.ts @@ -394,3 +394,16 @@ test.describe('Pathless layout routes', () => { await expect(page.locator('body')).toContainText('Not Found') }) }) + +test.describe('Special characters in route paths', () => { + test('should render route with pipe character in path', async ({ + page, + baseURL, + }) => { + await page.goto('/pipe/hello|world') + + await expect(page.locator('body')).toContainText('Hello hello|world!') + + expect(page.url()).toBe(`${baseURL}/pipe/hello%7Cworld`) + }) +}) diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts index 27d5e91145..2a72e41265 100644 --- a/e2e/react-start/basic/src/routeTree.gen.ts +++ b/e2e/react-start/basic/src/routeTree.gen.ts @@ -42,6 +42,7 @@ import { Route as RawStreamSsrMixedRouteImport } from './routes/raw-stream/ssr-m import { Route as RawStreamSsrBinaryHintRouteImport } from './routes/raw-stream/ssr-binary-hint' import { Route as RawStreamClientCallRouteImport } from './routes/raw-stream/client-call' import { Route as PostsPostIdRouteImport } from './routes/posts.$postId' +import { Route as PipeReferenceRouteImport } from './routes/pipe.$reference' import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader' import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad' import { Route as MultiCookieRedirectTargetRouteImport } from './routes/multi-cookie-redirect/target' @@ -228,6 +229,11 @@ const PostsPostIdRoute = PostsPostIdRouteImport.update({ path: '/$postId', getParentRoute: () => PostsRoute, } as any) +const PipeReferenceRoute = PipeReferenceRouteImport.update({ + id: '/pipe/$reference', + path: '/pipe/$reference', + getParentRoute: () => rootRouteImport, +} as any) const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({ id: '/via-loader', path: '/via-loader', @@ -343,6 +349,7 @@ export interface FileRoutesByFullPath { '/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute '/not-found/via-loader': typeof NotFoundViaLoaderRoute + '/pipe/$reference': typeof PipeReferenceRoute '/posts/$postId': typeof PostsPostIdRoute '/raw-stream/client-call': typeof RawStreamClientCallRoute '/raw-stream/ssr-binary-hint': typeof RawStreamSsrBinaryHintRoute @@ -389,6 +396,7 @@ export interface FileRoutesByTo { '/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute '/not-found/via-loader': typeof NotFoundViaLoaderRoute + '/pipe/$reference': typeof PipeReferenceRoute '/posts/$postId': typeof PostsPostIdRoute '/raw-stream/client-call': typeof RawStreamClientCallRoute '/raw-stream/ssr-binary-hint': typeof RawStreamSsrBinaryHintRoute @@ -441,6 +449,7 @@ export interface FileRoutesById { '/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute '/not-found/via-loader': typeof NotFoundViaLoaderRoute + '/pipe/$reference': typeof PipeReferenceRoute '/posts/$postId': typeof PostsPostIdRoute '/raw-stream/client-call': typeof RawStreamClientCallRoute '/raw-stream/ssr-binary-hint': typeof RawStreamSsrBinaryHintRoute @@ -494,6 +503,7 @@ export interface FileRouteTypes { | '/multi-cookie-redirect/target' | '/not-found/via-beforeLoad' | '/not-found/via-loader' + | '/pipe/$reference' | '/posts/$postId' | '/raw-stream/client-call' | '/raw-stream/ssr-binary-hint' @@ -540,6 +550,7 @@ export interface FileRouteTypes { | '/multi-cookie-redirect/target' | '/not-found/via-beforeLoad' | '/not-found/via-loader' + | '/pipe/$reference' | '/posts/$postId' | '/raw-stream/client-call' | '/raw-stream/ssr-binary-hint' @@ -591,6 +602,7 @@ export interface FileRouteTypes { | '/multi-cookie-redirect/target' | '/not-found/via-beforeLoad' | '/not-found/via-loader' + | '/pipe/$reference' | '/posts/$postId' | '/raw-stream/client-call' | '/raw-stream/ssr-binary-hint' @@ -642,6 +654,7 @@ export interface RootRouteChildren { Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route ApiUsersRoute: typeof ApiUsersRouteWithChildren MultiCookieRedirectTargetRoute: typeof MultiCookieRedirectTargetRoute + PipeReferenceRoute: typeof PipeReferenceRoute RedirectTargetRoute: typeof RedirectTargetRouteWithChildren MultiCookieRedirectIndexRoute: typeof MultiCookieRedirectIndexRoute RedirectIndexRoute: typeof RedirectIndexRoute @@ -882,6 +895,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PostsPostIdRouteImport parentRoute: typeof PostsRoute } + '/pipe/$reference': { + id: '/pipe/$reference' + path: '/pipe/$reference' + fullPath: '/pipe/$reference' + preLoaderRoute: typeof PipeReferenceRouteImport + parentRoute: typeof rootRouteImport + } '/not-found/via-loader': { id: '/not-found/via-loader' path: '/via-loader' @@ -1184,6 +1204,7 @@ const rootRouteChildren: RootRouteChildren = { Char45824Char54620Char48124Char44397Route, ApiUsersRoute: ApiUsersRouteWithChildren, MultiCookieRedirectTargetRoute: MultiCookieRedirectTargetRoute, + PipeReferenceRoute: PipeReferenceRoute, RedirectTargetRoute: RedirectTargetRouteWithChildren, MultiCookieRedirectIndexRoute: MultiCookieRedirectIndexRoute, RedirectIndexRoute: RedirectIndexRoute, diff --git a/e2e/react-start/basic/src/routes/__root.tsx b/e2e/react-start/basic/src/routes/__root.tsx index e1862b499c..145b9ff76d 100644 --- a/e2e/react-start/basic/src/routes/__root.tsx +++ b/e2e/react-start/basic/src/routes/__root.tsx @@ -181,6 +181,15 @@ function RootDocument({ children }: { children: React.ReactNode }) { > Raw Stream {' '} + + Dynamic Pipe Character + {' '} Hello {params.reference}! +} diff --git a/e2e/react-start/basic/tests/pipe-character-in-path.spec.ts b/e2e/react-start/basic/tests/pipe-character-in-path.spec.ts new file mode 100644 index 0000000000..0c57126c64 --- /dev/null +++ b/e2e/react-start/basic/tests/pipe-character-in-path.spec.ts @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test' + +import { test } from '@tanstack/router-e2e-utils' + +test('encodes pipe character in href for param link', async ({ page }) => { + await page.goto('/pipe/hello|world') + + await expect(page.locator('body')).toContainText('Hello hello|world!') +}) + +test('direct navigation keeps encoded url after reload', async ({ + page, + baseURL, +}) => { + await page.goto('/pipe/hello|world') + + await expect(page.locator('body')).toContainText('Hello hello|world!') + expect(page.url()).toBe(`${baseURL}/pipe/hello%7Cworld`) + + await page.reload() + + await expect(page.locator('body')).toContainText('Hello hello|world!') + expect(page.url()).toBe(`${baseURL}/pipe/hello%7Cworld`) +}) diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index 859a900496..7f382fa6bd 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -19,10 +19,10 @@ export function joinPaths(paths: Array) { ) } -/** Remove repeated slashes from a path string. */ +/** Remove repeated slashes and replace | to %7C from a path string. */ export function cleanPath(path: string) { // remove double slashes - return path.replace(/\/{2,}/g, '/') + return path.replace(/\/{2,}/g, '/').replace(/\|/g, '%7C') } /** Trim leading slashes (except preserving root '/'). */