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 efb0b4ef6cc..a84b234638b 100644
--- a/e2e/react-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/react-router/basic-file-based/src/routeTree.gen.ts
@@ -9,7 +9,6 @@
// 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 Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
import { Route as RemountDepsRouteImport } from './routes/remountDeps'
import { Route as PostsRouteImport } from './routes/posts'
import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
@@ -18,6 +17,7 @@ import { Route as EditingARouteImport } from './routes/editing-a'
import { Route as ComponentTypesTestRouteImport } from './routes/component-types-test'
import { Route as AnchorRouteImport } from './routes/anchor'
import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as Char45824Char54620Char48124Char44397RouteRouteImport } from './routes/대한민국/route'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
import { Route as NonNestedRouteRouteImport } from './routes/non-nested/route'
import { Route as IndexRouteImport } from './routes/index'
@@ -45,6 +45,8 @@ import { Route as NonNestedNamedRouteRouteImport } from './routes/non-nested/nam
import { Route as RedirectTargetIndexRouteImport } from './routes/redirect/$target/index'
import { Route as ParamsPsWildcardIndexRouteImport } from './routes/params-ps/wildcard/index'
import { Route as ParamsPsNamedIndexRouteImport } from './routes/params-ps/named/index'
+import { Route as Char45824Char54620Char48124Char44397Char55357Char56960IdRouteImport } from './routes/대한민국/🚀.$id'
+import { Route as Char45824Char54620Char48124Char44397WildcardSplatRouteImport } from './routes/대한민국/wildcard.$'
import { Route as RelativeUseNavigateRelativeUseNavigateBRouteImport } from './routes/relative/useNavigate/relative-useNavigate-b'
import { Route as RelativeUseNavigateRelativeUseNavigateARouteImport } from './routes/relative/useNavigate/relative-useNavigate-a'
import { Route as RelativeLinkRelativeLinkBRouteImport } from './routes/relative/link/relative-link-b'
@@ -99,12 +101,6 @@ import { Route as RelativeLinkPathPathIndexRouteImport } from './routes/relative
import { Route as RelativeLinkNestedDeepIndexRouteImport } from './routes/relative/link/nested/deep/index'
import { Route as ParamsPsNamedFooBarBazRouteImport } from './routes/params-ps/named/$foo/$bar.$baz'
-const Char45824Char54620Char48124Char44397Route =
- Char45824Char54620Char48124Char44397RouteImport.update({
- id: '/대한민국',
- path: '/대한민국',
- getParentRoute: () => rootRouteImport,
- } as any)
const RemountDepsRoute = RemountDepsRouteImport.update({
id: '/remountDeps',
path: '/remountDeps',
@@ -144,6 +140,12 @@ const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
+const Char45824Char54620Char48124Char44397RouteRoute =
+ Char45824Char54620Char48124Char44397RouteRouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any)
const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
id: '/search-params',
path: '/search-params',
@@ -282,6 +284,18 @@ const ParamsPsNamedIndexRoute = ParamsPsNamedIndexRouteImport.update({
path: '/params-ps/named/',
getParentRoute: () => rootRouteImport,
} as any)
+const Char45824Char54620Char48124Char44397Char55357Char56960IdRoute =
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRouteImport.update({
+ id: '/🚀/$id',
+ path: '/🚀/$id',
+ getParentRoute: () => Char45824Char54620Char48124Char44397RouteRoute,
+ } as any)
+const Char45824Char54620Char48124Char44397WildcardSplatRoute =
+ Char45824Char54620Char48124Char44397WildcardSplatRouteImport.update({
+ id: '/wildcard/$',
+ path: '/wildcard/$',
+ getParentRoute: () => Char45824Char54620Char48124Char44397RouteRoute,
+ } as any)
const RelativeUseNavigateRelativeUseNavigateBRoute =
RelativeUseNavigateRelativeUseNavigateBRouteImport.update({
id: '/relative-useNavigate-b',
@@ -581,6 +595,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
'/editing-a': typeof EditingARoute
@@ -588,7 +603,6 @@ export interface FileRoutesByFullPath {
'/notRemountDeps': typeof NotRemountDepsRoute
'/posts': typeof PostsRouteWithChildren
'/remountDeps': typeof RemountDepsRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/non-nested/named': typeof NonNestedNamedRouteRouteWithChildren
'/non-nested/path': typeof NonNestedPathRouteRouteWithChildren
'/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren
@@ -636,6 +650,8 @@ export interface FileRoutesByFullPath {
'/relative/link/relative-link-b': typeof RelativeLinkRelativeLinkBRoute
'/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
+ '/대한민국/wildcard/$': typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ '/대한민국/🚀/$id': typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
'/params-ps/named': typeof ParamsPsNamedIndexRoute
'/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
@@ -668,13 +684,13 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/non-nested': typeof NonNestedRouteRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
'/editing-a': typeof EditingARoute
'/editing-b': typeof EditingBRoute
'/notRemountDeps': typeof NotRemountDepsRoute
'/remountDeps': typeof RemountDepsRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/non-nested/named': typeof NonNestedNamedRouteRouteWithChildren
'/non-nested/path': typeof NonNestedPathRouteRouteWithChildren
'/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren
@@ -717,6 +733,8 @@ export interface FileRoutesByTo {
'/relative/link/relative-link-b': typeof RelativeLinkRelativeLinkBRoute
'/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
+ '/대한민국/wildcard/$': typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ '/대한민국/🚀/$id': typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
'/params-ps/named': typeof ParamsPsNamedIndexRoute
'/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute
'/redirect/$target': typeof RedirectTargetIndexRoute
@@ -751,6 +769,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
@@ -759,7 +778,6 @@ export interface FileRoutesById {
'/notRemountDeps': typeof NotRemountDepsRoute
'/posts': typeof PostsRouteWithChildren
'/remountDeps': typeof RemountDepsRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/non-nested/named': typeof NonNestedNamedRouteRouteWithChildren
'/non-nested/path': typeof NonNestedPathRouteRouteWithChildren
'/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren
@@ -809,6 +827,8 @@ export interface FileRoutesById {
'/relative/link/relative-link-b': typeof RelativeLinkRelativeLinkBRoute
'/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
+ '/대한민국/wildcard/$': typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ '/대한민국/🚀/$id': typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
'/params-ps/named/': typeof ParamsPsNamedIndexRoute
'/params-ps/wildcard/': typeof ParamsPsWildcardIndexRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
@@ -844,6 +864,7 @@ export interface FileRouteTypes {
| '/'
| '/non-nested'
| '/search-params'
+ | '/대한민국'
| '/anchor'
| '/component-types-test'
| '/editing-a'
@@ -851,7 +872,6 @@ export interface FileRouteTypes {
| '/notRemountDeps'
| '/posts'
| '/remountDeps'
- | '/대한민국'
| '/non-nested/named'
| '/non-nested/path'
| '/non-nested/prefix'
@@ -899,6 +919,8 @@ export interface FileRouteTypes {
| '/relative/link/relative-link-b'
| '/relative/useNavigate/relative-useNavigate-a'
| '/relative/useNavigate/relative-useNavigate-b'
+ | '/대한민국/wildcard/$'
+ | '/대한민국/🚀/$id'
| '/params-ps/named'
| '/params-ps/wildcard'
| '/redirect/$target/'
@@ -931,13 +953,13 @@ export interface FileRouteTypes {
to:
| '/'
| '/non-nested'
+ | '/대한민국'
| '/anchor'
| '/component-types-test'
| '/editing-a'
| '/editing-b'
| '/notRemountDeps'
| '/remountDeps'
- | '/대한민국'
| '/non-nested/named'
| '/non-nested/path'
| '/non-nested/prefix'
@@ -980,6 +1002,8 @@ export interface FileRouteTypes {
| '/relative/link/relative-link-b'
| '/relative/useNavigate/relative-useNavigate-a'
| '/relative/useNavigate/relative-useNavigate-b'
+ | '/대한민국/wildcard/$'
+ | '/대한민국/🚀/$id'
| '/params-ps/named'
| '/params-ps/wildcard'
| '/redirect/$target'
@@ -1013,6 +1037,7 @@ export interface FileRouteTypes {
| '/'
| '/non-nested'
| '/search-params'
+ | '/대한민국'
| '/_layout'
| '/anchor'
| '/component-types-test'
@@ -1021,7 +1046,6 @@ export interface FileRouteTypes {
| '/notRemountDeps'
| '/posts'
| '/remountDeps'
- | '/대한민국'
| '/non-nested/named'
| '/non-nested/path'
| '/non-nested/prefix'
@@ -1071,6 +1095,8 @@ export interface FileRouteTypes {
| '/relative/link/relative-link-b'
| '/relative/useNavigate/relative-useNavigate-a'
| '/relative/useNavigate/relative-useNavigate-b'
+ | '/대한민국/wildcard/$'
+ | '/대한민국/🚀/$id'
| '/params-ps/named/'
| '/params-ps/wildcard/'
| '/redirect/$target/'
@@ -1105,6 +1131,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
NonNestedRouteRoute: typeof NonNestedRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
+ Char45824Char54620Char48124Char44397RouteRoute: typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
AnchorRoute: typeof AnchorRoute
ComponentTypesTestRoute: typeof ComponentTypesTestRoute
@@ -1113,7 +1140,6 @@ export interface RootRouteChildren {
NotRemountDepsRoute: typeof NotRemountDepsRoute
PostsRoute: typeof PostsRouteWithChildren
RemountDepsRoute: typeof RemountDepsRoute
- Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
ParamsPsNonNestedRouteRoute: typeof ParamsPsNonNestedRouteRouteWithChildren
RelativeLinkRouteRoute: typeof RelativeLinkRouteRouteWithChildren
RelativeUseNavigateRouteRoute: typeof RelativeUseNavigateRouteRouteWithChildren
@@ -1146,13 +1172,6 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
- '/대한민국': {
- id: '/대한민국'
- path: '/대한민국'
- fullPath: '/대한민국'
- preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
- parentRoute: typeof rootRouteImport
- }
'/remountDeps': {
id: '/remountDeps'
path: '/remountDeps'
@@ -1209,6 +1228,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/search-params': {
id: '/search-params'
path: '/search-params'
@@ -1398,6 +1424,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ParamsPsNamedIndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/대한민국/🚀/$id': {
+ id: '/대한민국/🚀/$id'
+ path: '/🚀/$id'
+ fullPath: '/대한민국/🚀/$id'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRouteImport
+ parentRoute: typeof Char45824Char54620Char48124Char44397RouteRoute
+ }
+ '/대한민국/wildcard/$': {
+ id: '/대한민국/wildcard/$'
+ path: '/wildcard/$'
+ fullPath: '/대한민국/wildcard/$'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397WildcardSplatRouteImport
+ parentRoute: typeof Char45824Char54620Char48124Char44397RouteRoute
+ }
'/relative/useNavigate/relative-useNavigate-b': {
id: '/relative/useNavigate/relative-useNavigate-b'
path: '/relative-useNavigate-b'
@@ -1926,6 +1966,24 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface Char45824Char54620Char48124Char44397RouteRouteChildren {
+ Char45824Char54620Char48124Char44397WildcardSplatRoute: typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRoute: typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
+}
+
+const Char45824Char54620Char48124Char44397RouteRouteChildren: Char45824Char54620Char48124Char44397RouteRouteChildren =
+ {
+ Char45824Char54620Char48124Char44397WildcardSplatRoute:
+ Char45824Char54620Char48124Char44397WildcardSplatRoute,
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRoute:
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRoute,
+ }
+
+const Char45824Char54620Char48124Char44397RouteRouteWithChildren =
+ Char45824Char54620Char48124Char44397RouteRoute._addFileChildren(
+ Char45824Char54620Char48124Char44397RouteRouteChildren,
+ )
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -2104,6 +2162,8 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NonNestedRouteRoute: NonNestedRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ Char45824Char54620Char48124Char44397RouteRoute:
+ Char45824Char54620Char48124Char44397RouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
AnchorRoute: AnchorRoute,
ComponentTypesTestRoute: ComponentTypesTestRoute,
@@ -2112,8 +2172,6 @@ const rootRouteChildren: RootRouteChildren = {
NotRemountDepsRoute: NotRemountDepsRoute,
PostsRoute: PostsRouteWithChildren,
RemountDepsRoute: RemountDepsRoute,
- Char45824Char54620Char48124Char44397Route:
- Char45824Char54620Char48124Char44397Route,
ParamsPsNonNestedRouteRoute: ParamsPsNonNestedRouteRouteWithChildren,
RelativeLinkRouteRoute: RelativeLinkRouteRouteWithChildren,
RelativeUseNavigateRouteRoute: RelativeUseNavigateRouteRouteWithChildren,
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 06734a3b954..26cd6a60f85 100644
--- a/e2e/react-router/basic-file-based/src/routes/__root.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/__root.tsx
@@ -130,6 +130,14 @@ function RootComponent() {
>
relative routing
{' '}
+
+ unicode path
+ {' '}
+
+
+ /params-ps/named/$foo - with special characters
+
+
+
+
+ /params-ps/wildcard/$ with encoded params
+
+
Bar2
+
+ Bar with special characters
+
)
diff --git a/e2e/react-router/basic-file-based/src/routes/search-params/index.tsx b/e2e/react-router/basic-file-based/src/routes/search-params/index.tsx
index c0d4a55ac85..d56d5ddac14 100644
--- a/e2e/react-router/basic-file-based/src/routes/search-params/index.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/search-params/index.tsx
@@ -21,6 +21,23 @@ function RouteComponent() {
>
go to /search-params/default?default=d2
+
+
+ go to /search-params/default?default=🚀대한민국
+
+
+
+ go to
+ /search-params/default?default=%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD
+
)
}
diff --git a/e2e/react-router/basic-file-based/src/routes/search-params/route.tsx b/e2e/react-router/basic-file-based/src/routes/search-params/route.tsx
index 5adb281c41b..3f659c812d2 100644
--- a/e2e/react-router/basic-file-based/src/routes/search-params/route.tsx
+++ b/e2e/react-router/basic-file-based/src/routes/search-params/route.tsx
@@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/search-params')({
beforeLoad: async () => {
- await new Promise((resolve) => setTimeout(resolve, 1000))
+ await new Promise((resolve) => setTimeout(resolve, 100))
return { hello: 'world' as string }
},
})
diff --git "a/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
deleted file mode 100644
index c70cb5096a9..00000000000
--- "a/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createFileRoute } from '@tanstack/react-router'
-
-export const Route = createFileRoute('/대한민국')({
- component: RouteComponent,
-})
-
-function RouteComponent() {
- return Hello "/대한민국"!
-}
diff --git "a/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/route.tsx" "b/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/route.tsx"
new file mode 100644
index 00000000000..cff4d82723f
--- /dev/null
+++ "b/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/route.tsx"
@@ -0,0 +1,63 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/대한민국')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+ Hello "/대한민국"!
+
+
+ -
+
+ link to latin id
+
+
+ -
+
+ link to unicode id
+
+
+ -
+
+ link to foo/bar
+
+
+ -
+
+ link to foo%\/🚀대
+
+
+
+
+
+
+ )
+}
diff --git "a/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/wildcard.$.tsx" "b/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/wildcard.$.tsx"
new file mode 100644
index 00000000000..5a0770344b9
--- /dev/null
+++ "b/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/wildcard.$.tsx"
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/대한민국/wildcard/$')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+
+ return (
+
+
Unicode Wildcard Params
+
+ Hello /대한민국/wildcard/
+ {params._splat}
+
+
+ )
+}
diff --git "a/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/\360\237\232\200.$id.tsx" "b/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/\360\237\232\200.$id.tsx"
new file mode 100644
index 00000000000..c8222486709
--- /dev/null
+++ "b/e2e/react-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/\360\237\232\200.$id.tsx"
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/대한민국/🚀/$id')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+
+ return (
+
+
Unicode Named Params
+
+ Hello /대한민국/🚀/
+ {params.id}
+
+
+ )
+}
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 46123e7d2eb..ab11a5fb1c6 100644
--- a/e2e/react-router/basic-file-based/tests/app.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/app.spec.ts
@@ -316,3 +316,13 @@ test('Should remount deps when remountDeps does change ', async ({ page }) => {
'Page component mounts: 3',
)
})
+
+test.describe('Unicode route rendering', () => {
+ test('should render non-latin route correctly', async ({ page, baseURL }) => {
+ await page.goto('/대한민국')
+
+ await expect(page.locator('body')).toContainText('Hello "/대한민국"!')
+
+ expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
+ })
+})
diff --git a/e2e/react-router/basic-file-based/tests/params.spec.ts b/e2e/react-router/basic-file-based/tests/params.spec.ts
index 499c8b97e01..c05dae91e05 100644
--- a/e2e/react-router/basic-file-based/tests/params.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/params.spec.ts
@@ -145,6 +145,12 @@ test.describe('params operations + prefix/suffix', () => {
params: { foo: 'foo' },
destHeadingId: 'ParamsNamedFooSuffix',
},
+ {
+ id: 'l-to-named-foo-special-characters',
+ pathname: '/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80',
+ params: { foo: 'foo%\\/🚀대' },
+ destHeadingId: 'ParamsNamedFoo',
+ },
] satisfies Array<{
id: string
pathname: string
@@ -317,6 +323,16 @@ test.describe('params operations + prefix/suffix', () => {
},
destHeadingId: 'ParamsWildcardSplatSuffix',
},
+ {
+ id: 'l-to-wildcard-encoded',
+ pathname:
+ '/params-ps/wildcard/%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ params: {
+ '*': '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ _splat: '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ },
+ destHeadingId: 'ParamsWildcardSplat',
+ },
] satisfies Array<{
id: string
pathname: string
@@ -368,12 +384,122 @@ test.describe('params operations + prefix/suffix', () => {
})
})
-test.describe('Unicode route rendering', () => {
- test('should render non-latin route correctly', async ({ page, baseURL }) => {
- await page.goto('/대한민국')
+test.describe('Unicode params', () => {
+ test('should render non-latin route correctly across multiple params', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/params-ps')
+ await page.waitForURL('/params-ps')
+ const fooLink = page.getByTestId('l-to-named-foo-special-characters')
+
+ await fooLink.click()
+ await page.waitForURL('/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80')
+
+ expect(page.url()).toBe(
+ `${baseURL}/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80`,
+ )
+
+ const headingEl = page.getByRole('heading', { name: 'ParamsNamedFoo' })
+ await expect(headingEl).toBeVisible()
+ let paramsEl = page.getByTestId('params-output')
+ let paramsText = await paramsEl.innerText()
+ expect(paramsText).toEqual(JSON.stringify({ foo: 'foo%\\/🚀대' }))
+
+ const barLink = page.getByTestId('params-foo-links-bar-special-characters')
+
+ await barLink.click()
+
+ await page.waitForURL(
+ '/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80/%F0%9F%9A%80%252F%2Fabc%EB%8C%80',
+ )
+
+ expect(page.url()).toBe(
+ `${baseURL}/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80/%F0%9F%9A%80%252F%2Fabc%EB%8C%80`,
+ )
+
+ paramsEl = page.getByTestId('foo-bar-value')
+ paramsText = await paramsEl.innerText()
+ expect(paramsText).toEqual('🚀%2F/abc대')
+ })
+
+ test.describe('should handle routes with non-latin paths and params correctly', () => {
+ const testCases = [
+ {
+ name: 'named',
+ childPath: '🚀',
+ latinParams: 'foo',
+ unicodeParams: 'foo%\\/🚀대',
+ },
+ {
+ name: 'wildcard',
+ childPath: 'wildcard',
+ latinParams: 'foo/bar',
+ unicodeParams: 'foo%\\/🚀대',
+ },
+ ]
+
+ testCases.forEach(({ name, childPath, latinParams, unicodeParams }) => {
+ test(`${name} params`, async ({ page, baseURL }) => {
+ const pascalCaseName = name.charAt(0).toUpperCase() + name.slice(1)
+ const routeParentPath = '/대한민국'
+ const encodedRouteParentPath = encodeURI(routeParentPath)
+ const childRoutePath = `${routeParentPath}/${childPath}`
+ const encodedChildRoutePath = encodeURI(childRoutePath)
+
+ await page.goto(routeParentPath)
+ await page.waitForURL(encodedRouteParentPath)
+
+ const headingRootEl = page.getByTestId('unicode-heading')
+
+ expect(await headingRootEl.innerText()).toBe('Hello "/대한민국"!')
+
+ const latinLink = page.getByTestId(`l-to-${name}-latin`)
+ const unicodeLink = page.getByTestId(`l-to-${name}-unicode`)
+
+ await expect(latinLink).not.toContainClass('font-bold')
+ await expect(unicodeLink).not.toContainClass('font-bold')
+
+ await latinLink.click()
- await expect(page.locator('body')).toContainText('Hello "/대한민국"!')
+ await page.waitForURL(`${encodedChildRoutePath}/${latinParams}`)
- expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
+ expect(page.url()).toBe(
+ `${baseURL}${encodedChildRoutePath}/${latinParams}`,
+ )
+
+ await expect(latinLink).toContainClass('font-bold')
+ await expect(unicodeLink).not.toContainClass('font-bold')
+
+ const headingEl = page.getByTestId(`unicode-${name}-heading`)
+ const paramsEl = page.getByTestId(`unicode-${name}-params`)
+
+ expect(await headingEl.innerText()).toBe(
+ `Unicode ${pascalCaseName} Params`,
+ )
+ expect(await paramsEl.innerText()).toBe(latinParams)
+
+ await unicodeLink.click()
+
+ const encodedParams =
+ name === 'wildcard'
+ ? encodeURI(unicodeParams)
+ : encodeURIComponent(unicodeParams)
+
+ await page.waitForURL(`${encodedChildRoutePath}/${encodedParams}`)
+
+ expect(page.url()).toBe(
+ `${baseURL}${encodedChildRoutePath}/${encodedParams}`,
+ )
+
+ await expect(latinLink).not.toContainClass('font-bold')
+ await expect(unicodeLink).toContainClass('font-bold')
+
+ expect(await headingEl.innerText()).toBe(
+ `Unicode ${pascalCaseName} Params`,
+ )
+ expect(await paramsEl.innerText()).toBe(unicodeParams)
+ })
+ })
})
})
diff --git a/e2e/react-router/basic-file-based/tests/search-params.spec.ts b/e2e/react-router/basic-file-based/tests/search-params.spec.ts
index 1fb0fc3034b..d6307cbcf8b 100644
--- a/e2e/react-router/basic-file-based/tests/search-params.spec.ts
+++ b/e2e/react-router/basic-file-based/tests/search-params.spec.ts
@@ -5,6 +5,7 @@ test.describe('/search-params/default', () => {
page,
}) => {
await page.goto('/search-params/default')
+ await page.waitForURL('/search-params/default?default=d1')
await expect(page.getByTestId('search-default')).toContainText('d1')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -17,6 +18,7 @@ test.describe('/search-params/default', () => {
page,
}) => {
await page.goto('/search-params/default/?default=d2')
+ await page.waitForURL('/search-params/default?default=d2')
await expect(page.getByTestId('search-default')).toContainText('d2')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -25,9 +27,54 @@ test.describe('/search-params/default', () => {
).toBeTruthy()
})
+ test('Directly visiting the route with special character search param set', async ({
+ page,
+ }) => {
+ await page.goto('/search-params/default/?default=🚀대한민국')
+ await page.waitForURL(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText('🚀대한민국')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ ),
+ ).toBeTruthy()
+ })
+
+ test('Directly visiting the route with encoded character search param set', async ({
+ page,
+ }) => {
+ await page.goto(
+ '/search-params/default/?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ )
+ await page.waitForURL(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText(
+ '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ ),
+ ).toBeTruthy()
+ })
+
test('navigating to the route without search param set', async ({ page }) => {
await page.goto('/search-params/')
await page.getByTestId('link-to-default-without-search').click()
+ await page.waitForURL('/search-params/default?default=d1')
await expect(page.getByTestId('search-default')).toContainText('d1')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -39,6 +86,7 @@ test.describe('/search-params/default', () => {
test('navigating to the route with search param set', async ({ page }) => {
await page.goto('/search-params/')
await page.getByTestId('link-to-default-with-search').click()
+ await page.waitForURL('/search-params/default?default=d2')
await expect(page.getByTestId('search-default')).toContainText('d2')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -46,4 +94,50 @@ test.describe('/search-params/default', () => {
page.url().endsWith('/search-params/default?default=d2'),
).toBeTruthy()
})
+
+ test('navigating to the route with special character search param set', async ({
+ page,
+ }) => {
+ await page.goto('/search-params/')
+ await page
+ .getByTestId('link-to-default-with-search-special-characters')
+ .click()
+ await page.waitForURL(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText('🚀대한민국')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ ),
+ ).toBeTruthy()
+ })
+
+ test('navigating to the route with encoded character search param set', async ({
+ page,
+ }) => {
+ await page.goto('/search-params/')
+ await page
+ .getByTestId('link-to-default-with-search-encoded-characters')
+ .click()
+ await page.waitForURL(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText(
+ '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ ),
+ ).toBeTruthy()
+ })
})
diff --git a/e2e/solid-router/basic-file-based/src/routeTree.gen.ts b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts
index 544ba70d76e..bfa9054cffe 100644
--- a/e2e/solid-router/basic-file-based/src/routeTree.gen.ts
+++ b/e2e/solid-router/basic-file-based/src/routeTree.gen.ts
@@ -9,7 +9,6 @@
// 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 Char45824Char54620Char48124Char44397RouteImport } from './routes/대한민국'
import { Route as RemountDepsRouteImport } from './routes/remountDeps'
import { Route as PostsRouteImport } from './routes/posts'
import { Route as NotRemountDepsRouteImport } from './routes/notRemountDeps'
@@ -18,6 +17,7 @@ import { Route as EditingARouteImport } from './routes/editing-a'
import { Route as ComponentTypesTestRouteImport } from './routes/component-types-test'
import { Route as AnchorRouteImport } from './routes/anchor'
import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as Char45824Char54620Char48124Char44397RouteRouteImport } from './routes/대한민국/route'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
import { Route as NonNestedRouteRouteImport } from './routes/non-nested/route'
import { Route as IndexRouteImport } from './routes/index'
@@ -45,6 +45,8 @@ import { Route as NonNestedNamedRouteRouteImport } from './routes/non-nested/nam
import { Route as RedirectTargetIndexRouteImport } from './routes/redirect/$target/index'
import { Route as ParamsPsWildcardIndexRouteImport } from './routes/params-ps/wildcard/index'
import { Route as ParamsPsNamedIndexRouteImport } from './routes/params-ps/named/index'
+import { Route as Char45824Char54620Char48124Char44397Char55357Char56960IdRouteImport } from './routes/대한민국/🚀.$id'
+import { Route as Char45824Char54620Char48124Char44397WildcardSplatRouteImport } from './routes/대한민국/wildcard.$'
import { Route as RelativeUseNavigateRelativeUseNavigateBRouteImport } from './routes/relative/useNavigate/relative-useNavigate-b'
import { Route as RelativeUseNavigateRelativeUseNavigateARouteImport } from './routes/relative/useNavigate/relative-useNavigate-a'
import { Route as RelativeLinkRelativeLinkBRouteImport } from './routes/relative/link/relative-link-b'
@@ -99,12 +101,6 @@ import { Route as RelativeLinkPathPathIndexRouteImport } from './routes/relative
import { Route as RelativeLinkNestedDeepIndexRouteImport } from './routes/relative/link/nested/deep/index'
import { Route as ParamsPsNamedFooBarBazRouteImport } from './routes/params-ps/named/$foo/$bar.$baz'
-const Char45824Char54620Char48124Char44397Route =
- Char45824Char54620Char48124Char44397RouteImport.update({
- id: '/대한민국',
- path: '/대한민국',
- getParentRoute: () => rootRouteImport,
- } as any)
const RemountDepsRoute = RemountDepsRouteImport.update({
id: '/remountDeps',
path: '/remountDeps',
@@ -144,6 +140,12 @@ const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
+const Char45824Char54620Char48124Char44397RouteRoute =
+ Char45824Char54620Char48124Char44397RouteRouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => rootRouteImport,
+ } as any)
const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
id: '/search-params',
path: '/search-params',
@@ -281,6 +283,18 @@ const ParamsPsNamedIndexRoute = ParamsPsNamedIndexRouteImport.update({
path: '/params-ps/named/',
getParentRoute: () => rootRouteImport,
} as any)
+const Char45824Char54620Char48124Char44397Char55357Char56960IdRoute =
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRouteImport.update({
+ id: '/🚀/$id',
+ path: '/🚀/$id',
+ getParentRoute: () => Char45824Char54620Char48124Char44397RouteRoute,
+ } as any)
+const Char45824Char54620Char48124Char44397WildcardSplatRoute =
+ Char45824Char54620Char48124Char44397WildcardSplatRouteImport.update({
+ id: '/wildcard/$',
+ path: '/wildcard/$',
+ getParentRoute: () => Char45824Char54620Char48124Char44397RouteRoute,
+ } as any)
const RelativeUseNavigateRelativeUseNavigateBRoute =
RelativeUseNavigateRelativeUseNavigateBRouteImport.update({
id: '/relative-useNavigate-b',
@@ -580,6 +594,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
'/editing-a': typeof EditingARoute
@@ -587,7 +602,6 @@ export interface FileRoutesByFullPath {
'/notRemountDeps': typeof NotRemountDepsRoute
'/posts': typeof PostsRouteWithChildren
'/remountDeps': typeof RemountDepsRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/non-nested/named': typeof NonNestedNamedRouteRouteWithChildren
'/non-nested/path': typeof NonNestedPathRouteRouteWithChildren
'/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren
@@ -635,6 +649,8 @@ export interface FileRoutesByFullPath {
'/relative/link/relative-link-b': typeof RelativeLinkRelativeLinkBRoute
'/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
+ '/대한민국/wildcard/$': typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ '/대한민국/🚀/$id': typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
'/params-ps/named': typeof ParamsPsNamedIndexRoute
'/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
@@ -667,13 +683,13 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/non-nested': typeof NonNestedRouteRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
'/editing-a': typeof EditingARoute
'/editing-b': typeof EditingBRoute
'/notRemountDeps': typeof NotRemountDepsRoute
'/remountDeps': typeof RemountDepsRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/non-nested/named': typeof NonNestedNamedRouteRouteWithChildren
'/non-nested/path': typeof NonNestedPathRouteRouteWithChildren
'/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren
@@ -716,6 +732,8 @@ export interface FileRoutesByTo {
'/relative/link/relative-link-b': typeof RelativeLinkRelativeLinkBRoute
'/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
+ '/대한민국/wildcard/$': typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ '/대한민국/🚀/$id': typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
'/params-ps/named': typeof ParamsPsNamedIndexRoute
'/params-ps/wildcard': typeof ParamsPsWildcardIndexRoute
'/redirect/$target': typeof RedirectTargetIndexRoute
@@ -750,6 +768,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/non-nested': typeof NonNestedRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/대한민국': typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/anchor': typeof AnchorRoute
'/component-types-test': typeof ComponentTypesTestRoute
@@ -758,7 +777,6 @@ export interface FileRoutesById {
'/notRemountDeps': typeof NotRemountDepsRoute
'/posts': typeof PostsRouteWithChildren
'/remountDeps': typeof RemountDepsRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/non-nested/named': typeof NonNestedNamedRouteRouteWithChildren
'/non-nested/path': typeof NonNestedPathRouteRouteWithChildren
'/non-nested/prefix': typeof NonNestedPrefixRouteRouteWithChildren
@@ -808,6 +826,8 @@ export interface FileRoutesById {
'/relative/link/relative-link-b': typeof RelativeLinkRelativeLinkBRoute
'/relative/useNavigate/relative-useNavigate-a': typeof RelativeUseNavigateRelativeUseNavigateARoute
'/relative/useNavigate/relative-useNavigate-b': typeof RelativeUseNavigateRelativeUseNavigateBRoute
+ '/대한민국/wildcard/$': typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ '/대한민국/🚀/$id': typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
'/params-ps/named/': typeof ParamsPsNamedIndexRoute
'/params-ps/wildcard/': typeof ParamsPsWildcardIndexRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
@@ -843,6 +863,7 @@ export interface FileRouteTypes {
| '/'
| '/non-nested'
| '/search-params'
+ | '/대한민국'
| '/anchor'
| '/component-types-test'
| '/editing-a'
@@ -850,7 +871,6 @@ export interface FileRouteTypes {
| '/notRemountDeps'
| '/posts'
| '/remountDeps'
- | '/대한민국'
| '/non-nested/named'
| '/non-nested/path'
| '/non-nested/prefix'
@@ -898,6 +918,8 @@ export interface FileRouteTypes {
| '/relative/link/relative-link-b'
| '/relative/useNavigate/relative-useNavigate-a'
| '/relative/useNavigate/relative-useNavigate-b'
+ | '/대한민국/wildcard/$'
+ | '/대한민국/🚀/$id'
| '/params-ps/named'
| '/params-ps/wildcard'
| '/redirect/$target/'
@@ -930,13 +952,13 @@ export interface FileRouteTypes {
to:
| '/'
| '/non-nested'
+ | '/대한민국'
| '/anchor'
| '/component-types-test'
| '/editing-a'
| '/editing-b'
| '/notRemountDeps'
| '/remountDeps'
- | '/대한민국'
| '/non-nested/named'
| '/non-nested/path'
| '/non-nested/prefix'
@@ -979,6 +1001,8 @@ export interface FileRouteTypes {
| '/relative/link/relative-link-b'
| '/relative/useNavigate/relative-useNavigate-a'
| '/relative/useNavigate/relative-useNavigate-b'
+ | '/대한민국/wildcard/$'
+ | '/대한민국/🚀/$id'
| '/params-ps/named'
| '/params-ps/wildcard'
| '/redirect/$target'
@@ -1012,6 +1036,7 @@ export interface FileRouteTypes {
| '/'
| '/non-nested'
| '/search-params'
+ | '/대한민국'
| '/_layout'
| '/anchor'
| '/component-types-test'
@@ -1020,7 +1045,6 @@ export interface FileRouteTypes {
| '/notRemountDeps'
| '/posts'
| '/remountDeps'
- | '/대한민국'
| '/non-nested/named'
| '/non-nested/path'
| '/non-nested/prefix'
@@ -1070,6 +1094,8 @@ export interface FileRouteTypes {
| '/relative/link/relative-link-b'
| '/relative/useNavigate/relative-useNavigate-a'
| '/relative/useNavigate/relative-useNavigate-b'
+ | '/대한민국/wildcard/$'
+ | '/대한민국/🚀/$id'
| '/params-ps/named/'
| '/params-ps/wildcard/'
| '/redirect/$target/'
@@ -1104,6 +1130,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
NonNestedRouteRoute: typeof NonNestedRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
+ Char45824Char54620Char48124Char44397RouteRoute: typeof Char45824Char54620Char48124Char44397RouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
AnchorRoute: typeof AnchorRoute
ComponentTypesTestRoute: typeof ComponentTypesTestRoute
@@ -1112,7 +1139,6 @@ export interface RootRouteChildren {
NotRemountDepsRoute: typeof NotRemountDepsRoute
PostsRoute: typeof PostsRouteWithChildren
RemountDepsRoute: typeof RemountDepsRoute
- Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
ParamsPsNonNestedRouteRoute: typeof ParamsPsNonNestedRouteRouteWithChildren
RelativeLinkRouteRoute: typeof RelativeLinkRouteRouteWithChildren
RelativeUseNavigateRouteRoute: typeof RelativeUseNavigateRouteRouteWithChildren
@@ -1145,13 +1171,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/대한민국': {
- id: '/대한민국'
- path: '/대한민국'
- fullPath: '/대한민국'
- preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
- parentRoute: typeof rootRouteImport
- }
'/remountDeps': {
id: '/remountDeps'
path: '/remountDeps'
@@ -1208,6 +1227,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
+ '/대한민국': {
+ id: '/대한민국'
+ path: '/대한민국'
+ fullPath: '/대한민국'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/search-params': {
id: '/search-params'
path: '/search-params'
@@ -1397,6 +1423,20 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof ParamsPsNamedIndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/대한민국/🚀/$id': {
+ id: '/대한민국/🚀/$id'
+ path: '/🚀/$id'
+ fullPath: '/대한민국/🚀/$id'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRouteImport
+ parentRoute: typeof Char45824Char54620Char48124Char44397RouteRoute
+ }
+ '/대한민국/wildcard/$': {
+ id: '/대한민국/wildcard/$'
+ path: '/wildcard/$'
+ fullPath: '/대한민국/wildcard/$'
+ preLoaderRoute: typeof Char45824Char54620Char48124Char44397WildcardSplatRouteImport
+ parentRoute: typeof Char45824Char54620Char48124Char44397RouteRoute
+ }
'/relative/useNavigate/relative-useNavigate-b': {
id: '/relative/useNavigate/relative-useNavigate-b'
path: '/relative-useNavigate-b'
@@ -1925,6 +1965,24 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface Char45824Char54620Char48124Char44397RouteRouteChildren {
+ Char45824Char54620Char48124Char44397WildcardSplatRoute: typeof Char45824Char54620Char48124Char44397WildcardSplatRoute
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRoute: typeof Char45824Char54620Char48124Char44397Char55357Char56960IdRoute
+}
+
+const Char45824Char54620Char48124Char44397RouteRouteChildren: Char45824Char54620Char48124Char44397RouteRouteChildren =
+ {
+ Char45824Char54620Char48124Char44397WildcardSplatRoute:
+ Char45824Char54620Char48124Char44397WildcardSplatRoute,
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRoute:
+ Char45824Char54620Char48124Char44397Char55357Char56960IdRoute,
+ }
+
+const Char45824Char54620Char48124Char44397RouteRouteWithChildren =
+ Char45824Char54620Char48124Char44397RouteRoute._addFileChildren(
+ Char45824Char54620Char48124Char44397RouteRouteChildren,
+ )
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -2103,6 +2161,8 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NonNestedRouteRoute: NonNestedRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ Char45824Char54620Char48124Char44397RouteRoute:
+ Char45824Char54620Char48124Char44397RouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
AnchorRoute: AnchorRoute,
ComponentTypesTestRoute: ComponentTypesTestRoute,
@@ -2111,8 +2171,6 @@ const rootRouteChildren: RootRouteChildren = {
NotRemountDepsRoute: NotRemountDepsRoute,
PostsRoute: PostsRouteWithChildren,
RemountDepsRoute: RemountDepsRoute,
- Char45824Char54620Char48124Char44397Route:
- Char45824Char54620Char48124Char44397Route,
ParamsPsNonNestedRouteRoute: ParamsPsNonNestedRouteRouteWithChildren,
RelativeLinkRouteRoute: RelativeLinkRouteRouteWithChildren,
RelativeUseNavigateRouteRoute: RelativeUseNavigateRouteRouteWithChildren,
diff --git a/e2e/solid-router/basic-file-based/src/routes/__root.tsx b/e2e/solid-router/basic-file-based/src/routes/__root.tsx
index 0997c45b374..883fbd3406a 100644
--- a/e2e/solid-router/basic-file-based/src/routes/__root.tsx
+++ b/e2e/solid-router/basic-file-based/src/routes/__root.tsx
@@ -130,6 +130,14 @@ function RootComponent() {
>
relative routing
{' '}
+
+ unicode path
+ {' '}
+
+
+ /params-ps/named/$foo - with special characters
+
+
+
+
+ /params-ps/wildcard/$ with encoded params
+
+
Bar2
+
+ Bar with special characters
+
)
diff --git a/e2e/solid-router/basic-file-based/src/routes/search-params/index.tsx b/e2e/solid-router/basic-file-based/src/routes/search-params/index.tsx
index a99d64cbf44..baeb5ea6ff1 100644
--- a/e2e/solid-router/basic-file-based/src/routes/search-params/index.tsx
+++ b/e2e/solid-router/basic-file-based/src/routes/search-params/index.tsx
@@ -21,6 +21,23 @@ function RouteComponent() {
>
go to /search-params/default?default=d2
+
+
+ go to /search-params/default?default=🚀대한민국
+
+
+
+ go to
+ /search-params/default?default=%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD
+
)
}
diff --git a/e2e/solid-router/basic-file-based/src/routes/search-params/route.tsx b/e2e/solid-router/basic-file-based/src/routes/search-params/route.tsx
index 5d9fc675158..6efa7152b6a 100644
--- a/e2e/solid-router/basic-file-based/src/routes/search-params/route.tsx
+++ b/e2e/solid-router/basic-file-based/src/routes/search-params/route.tsx
@@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/solid-router'
export const Route = createFileRoute('/search-params')({
beforeLoad: async () => {
- await new Promise((resolve) => setTimeout(resolve, 1000))
+ await new Promise((resolve) => setTimeout(resolve, 100))
return { hello: 'world' as string }
},
})
diff --git "a/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
deleted file mode 100644
index 897c0576cc4..00000000000
--- "a/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createFileRoute } from '@tanstack/solid-router'
-
-export const Route = createFileRoute('/대한민국')({
- component: RouteComponent,
-})
-
-function RouteComponent() {
- return Hello "/대한민국"!
-}
diff --git "a/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/route.tsx" "b/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/route.tsx"
new file mode 100644
index 00000000000..e996d48c76b
--- /dev/null
+++ "b/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/route.tsx"
@@ -0,0 +1,63 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/대한민국')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+ Hello "/대한민국"!
+
+
+ -
+
+ link to latin id
+
+
+ -
+
+ link to unicode id
+
+
+ -
+
+ link to foo/bar
+
+
+ -
+
+ link to foo%\/🚀대
+
+
+
+
+
+
+ )
+}
diff --git "a/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/wildcard.$.tsx" "b/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/wildcard.$.tsx"
new file mode 100644
index 00000000000..d849f5675cb
--- /dev/null
+++ "b/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/wildcard.$.tsx"
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/대한민국/wildcard/$')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+
+ return (
+
+
Unicode Wildcard Params
+
+ Hello /대한민국/wildcard/
+ {params()._splat}
+
+
+ )
+}
diff --git "a/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/\360\237\232\200.$id.tsx" "b/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/\360\237\232\200.$id.tsx"
new file mode 100644
index 00000000000..93bb5e561c9
--- /dev/null
+++ "b/e2e/solid-router/basic-file-based/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255/\360\237\232\200.$id.tsx"
@@ -0,0 +1,19 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/대한민국/🚀/$id')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+
+ return (
+
+
Unicode Named Params
+
+ Hello /대한민국/🚀/
+ {params().id}
+
+
+ )
+}
diff --git a/e2e/solid-router/basic-file-based/tests/app.spec.ts b/e2e/solid-router/basic-file-based/tests/app.spec.ts
index 398021a7b94..f535fdd78ce 100644
--- a/e2e/solid-router/basic-file-based/tests/app.spec.ts
+++ b/e2e/solid-router/basic-file-based/tests/app.spec.ts
@@ -304,3 +304,13 @@ test('Should remount deps when remountDeps does change ', async ({ page }) => {
'Page component mounts: 3',
)
})
+
+test.describe('Unicode route rendering', () => {
+ test('should render non-latin route correctly', async ({ page, baseURL }) => {
+ await page.goto('/대한민국')
+
+ await expect(page.locator('body')).toContainText('Hello "/대한민국"!')
+
+ expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
+ })
+})
diff --git a/e2e/solid-router/basic-file-based/tests/params.spec.ts b/e2e/solid-router/basic-file-based/tests/params.spec.ts
index 499c8b97e01..9d57f437a72 100644
--- a/e2e/solid-router/basic-file-based/tests/params.spec.ts
+++ b/e2e/solid-router/basic-file-based/tests/params.spec.ts
@@ -145,6 +145,12 @@ test.describe('params operations + prefix/suffix', () => {
params: { foo: 'foo' },
destHeadingId: 'ParamsNamedFooSuffix',
},
+ {
+ id: 'l-to-named-foo-special-characters',
+ pathname: '/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80',
+ params: { foo: 'foo%\\/🚀대' },
+ destHeadingId: 'ParamsNamedFoo',
+ },
] satisfies Array<{
id: string
pathname: string
@@ -317,6 +323,16 @@ test.describe('params operations + prefix/suffix', () => {
},
destHeadingId: 'ParamsWildcardSplatSuffix',
},
+ {
+ id: 'l-to-wildcard-encoded',
+ pathname:
+ '/params-ps/wildcard/%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ params: {
+ '*': '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ _splat: '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ },
+ destHeadingId: 'ParamsWildcardSplat',
+ },
] satisfies Array<{
id: string
pathname: string
@@ -368,12 +384,123 @@ test.describe('params operations + prefix/suffix', () => {
})
})
-test.describe('Unicode route rendering', () => {
- test('should render non-latin route correctly', async ({ page, baseURL }) => {
- await page.goto('/대한민국')
+test.describe('Unicode params', () => {
+ test('should render non-latin route correctly across multiple params', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/params-ps')
+ await page.waitForURL('/params-ps')
+ const fooLink = page.getByTestId('l-to-named-foo-special-characters')
+
+ await fooLink.click()
+ await page.waitForURL('/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80')
+
+ expect(page.url()).toBe(
+ `${baseURL}/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80`,
+ )
- await expect(page.locator('body')).toContainText('Hello "/대한민국"!')
+ const headingEl = page.getByRole('heading', { name: 'ParamsNamedFoo' })
+ await expect(headingEl).toBeVisible()
+ let paramsEl = page.getByTestId('params-output')
+ let paramsText = await paramsEl.innerText()
+ expect(paramsText).toEqual(JSON.stringify({ foo: 'foo%\\/🚀대' }))
- expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
+ const barLink = page.getByTestId('params-foo-links-bar-special-characters')
+
+ await barLink.click()
+
+ await page.waitForURL(
+ '/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80/%F0%9F%9A%80%252F%2Fabc%EB%8C%80',
+ )
+
+ expect(page.url()).toBe(
+ `${baseURL}/params-ps/named/foo%25%5C%2F%F0%9F%9A%80%EB%8C%80/%F0%9F%9A%80%252F%2Fabc%EB%8C%80`,
+ )
+
+ paramsEl = page.getByTestId('foo-bar-value')
+ paramsText = await paramsEl.innerText()
+ expect(paramsText).toEqual('🚀%2F/abc대')
+ })
+
+ test.describe('should handle routes with non-latin paths and params correctly', () => {
+ const testCases = [
+ {
+ name: 'named',
+ childPath: '🚀',
+ latinParams: 'foo',
+ unicodeParams: 'foo%\\/🚀대',
+ },
+ {
+ name: 'wildcard',
+ childPath: 'wildcard',
+ latinParams: 'foo/bar',
+ unicodeParams: 'foo%\\/🚀대',
+ },
+ ]
+
+ testCases.forEach(({ name, childPath, latinParams, unicodeParams }) => {
+ test(`${name} params`, async ({ page, baseURL }) => {
+ const pascalCaseName = name.charAt(0).toUpperCase() + name.slice(1)
+ const routeParentPath = '/대한민국'
+ const encodedRouteParentPath = encodeURI(routeParentPath)
+ const childRoutePath = `${routeParentPath}/${childPath}`
+ const encodedChildRoutePath = encodeURI(childRoutePath)
+
+ await page.goto(routeParentPath)
+ await page.waitForURL(encodedRouteParentPath)
+
+ const headingRootEl = page.getByTestId('unicode-heading')
+
+ expect(await headingRootEl.innerText()).toBe('Hello "/대한민국"!')
+
+ const latinLink = page.getByTestId(`l-to-${name}-latin`)
+ const unicodeLink = page.getByTestId(`l-to-${name}-unicode`)
+
+ await expect(latinLink).not.toContainClass('font-bold')
+ await expect(unicodeLink).not.toContainClass('font-bold')
+
+ await latinLink.click()
+
+ await page.waitForURL(`${encodedChildRoutePath}/${latinParams}`)
+
+ expect(page.url()).toBe(
+ `${baseURL}${encodedChildRoutePath}/${latinParams}`,
+ )
+
+ await expect(latinLink).toContainClass('font-bold')
+ await expect(unicodeLink).not.toContainClass('font-bold')
+
+ const headingEl = page.getByTestId(`unicode-${name}-heading`)
+ const paramsEl = page.getByTestId(`unicode-${name}-params`)
+
+ expect(await headingEl.innerText()).toBe(
+ `Unicode ${pascalCaseName} Params`,
+ )
+
+ expect(await paramsEl.innerText()).toBe(latinParams)
+
+ await unicodeLink.click()
+
+ const encodedParams =
+ name === 'wildcard'
+ ? encodeURI(unicodeParams)
+ : encodeURIComponent(unicodeParams)
+
+ await page.waitForURL(`${encodedChildRoutePath}/${encodedParams}`)
+
+ expect(page.url()).toBe(
+ `${baseURL}${encodedChildRoutePath}/${encodedParams}`,
+ )
+
+ await expect(latinLink).not.toContainClass('font-bold')
+ await expect(unicodeLink).toContainClass('font-bold')
+
+ expect(await headingEl.innerText()).toBe(
+ `Unicode ${pascalCaseName} Params`,
+ )
+ expect(await paramsEl.innerText()).toBe(unicodeParams)
+ })
+ })
})
})
diff --git a/e2e/solid-router/basic-file-based/tests/search-params.spec.ts b/e2e/solid-router/basic-file-based/tests/search-params.spec.ts
index 1fb0fc3034b..d6307cbcf8b 100644
--- a/e2e/solid-router/basic-file-based/tests/search-params.spec.ts
+++ b/e2e/solid-router/basic-file-based/tests/search-params.spec.ts
@@ -5,6 +5,7 @@ test.describe('/search-params/default', () => {
page,
}) => {
await page.goto('/search-params/default')
+ await page.waitForURL('/search-params/default?default=d1')
await expect(page.getByTestId('search-default')).toContainText('d1')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -17,6 +18,7 @@ test.describe('/search-params/default', () => {
page,
}) => {
await page.goto('/search-params/default/?default=d2')
+ await page.waitForURL('/search-params/default?default=d2')
await expect(page.getByTestId('search-default')).toContainText('d2')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -25,9 +27,54 @@ test.describe('/search-params/default', () => {
).toBeTruthy()
})
+ test('Directly visiting the route with special character search param set', async ({
+ page,
+ }) => {
+ await page.goto('/search-params/default/?default=🚀대한민국')
+ await page.waitForURL(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText('🚀대한민국')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ ),
+ ).toBeTruthy()
+ })
+
+ test('Directly visiting the route with encoded character search param set', async ({
+ page,
+ }) => {
+ await page.goto(
+ '/search-params/default/?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ )
+ await page.waitForURL(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText(
+ '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ ),
+ ).toBeTruthy()
+ })
+
test('navigating to the route without search param set', async ({ page }) => {
await page.goto('/search-params/')
await page.getByTestId('link-to-default-without-search').click()
+ await page.waitForURL('/search-params/default?default=d1')
await expect(page.getByTestId('search-default')).toContainText('d1')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -39,6 +86,7 @@ test.describe('/search-params/default', () => {
test('navigating to the route with search param set', async ({ page }) => {
await page.goto('/search-params/')
await page.getByTestId('link-to-default-with-search').click()
+ await page.waitForURL('/search-params/default?default=d2')
await expect(page.getByTestId('search-default')).toContainText('d2')
await expect(page.getByTestId('context-hello')).toContainText('world')
@@ -46,4 +94,50 @@ test.describe('/search-params/default', () => {
page.url().endsWith('/search-params/default?default=d2'),
).toBeTruthy()
})
+
+ test('navigating to the route with special character search param set', async ({
+ page,
+ }) => {
+ await page.goto('/search-params/')
+ await page
+ .getByTestId('link-to-default-with-search-special-characters')
+ .click()
+ await page.waitForURL(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText('🚀대한민국')
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%F0%9F%9A%80%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ ),
+ ).toBeTruthy()
+ })
+
+ test('navigating to the route with encoded character search param set', async ({
+ page,
+ }) => {
+ await page.goto('/search-params/')
+ await page
+ .getByTestId('link-to-default-with-search-encoded-characters')
+ .click()
+ await page.waitForURL(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ )
+
+ await expect(page.getByTestId('search-default')).toContainText(
+ '%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD',
+ )
+ await expect(page.getByTestId('context-hello')).toContainText('world')
+ expect(
+ page
+ .url()
+ .endsWith(
+ '/search-params/default?default=%25EB%258C%2580%25ED%2595%259C%25EB%25AF%25BC%25EA%25B5%25AD',
+ ),
+ ).toBeTruthy()
+ })
})
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index 292d40918c8..bbb9fd8f042 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -6498,31 +6498,31 @@ describe('encoded and unicode paths', () => {
const testCases = [
{
name: 'with prefix',
- path: '/foo/prefix대{$}',
- browsePath: '/foo/prefix대test[s%5C/.%5C/parameter%25!🚀]',
+ path: '/foo/prefix@대{$}',
expectedPath:
- '/foo/prefix%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ '/foo/prefix@%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]',
+ expectedLocation: '/foo/prefix@대test[s%5C/.%5C/parameter%25!🚀@]',
params: {
- _splat: 'test[s\\/.\\/parameter%!🚀]',
- '*': 'test[s\\/.\\/parameter%!🚀]',
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
},
},
{
name: 'with suffix',
- path: '/foo/{$}대suffix',
- browsePath: '/foo/test[s%5C/.%5C/parameter%25!🚀]대suffix',
+ path: '/foo/{$}대suffix@',
expectedPath:
- '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]%EB%8C%80suffix',
+ '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]%EB%8C%80suffix@',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀@]대suffix@',
params: {
- _splat: 'test[s\\/.\\/parameter%!🚀]',
- '*': 'test[s\\/.\\/parameter%!🚀]',
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
},
},
{
name: 'with wildcard',
path: '/foo/$',
- browsePath: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
expectedPath: '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
params: {
_splat: 'test[s\\/.\\/parameter%!🚀]',
'*': 'test[s\\/.\\/parameter%!🚀]',
@@ -6532,8 +6532,8 @@ describe('encoded and unicode paths', () => {
{
name: 'with path param',
path: `/foo/$id`,
- browsePath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
expectedPath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
params: {
id: 'test[s\\/.\\/parameter%!🚀]',
},
@@ -6542,14 +6542,7 @@ describe('encoded and unicode paths', () => {
test.each(testCases)(
'should handle encoded, decoded paths with unicode characters correctly - $name',
- async ({ path, browsePath, expectedPath, params }) => {
- async function validate() {
- const paramsToValidate = await screen.findByTestId('params-to-validate')
-
- expect(window.location.pathname).toBe(expectedPath)
- expect(paramsToValidate.textContent).toEqual(JSON.stringify(params))
- }
-
+ async ({ path, expectedPath, expectedLocation, params }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
@@ -6595,19 +6588,18 @@ describe('encoded and unicode paths', () => {
render()
- await act(() => history.push(browsePath))
-
- await validate()
-
- await act(() => history.push('/'))
-
const link = await screen.findByTestId('link-to-path')
expect(link.getAttribute('href')).toBe(expectedPath)
await act(() => fireEvent.click(link))
- await validate()
+ const paramsToValidate = await screen.findByTestId('params-to-validate')
+
+ expect(window.location.pathname).toBe(expectedPath)
+ expect(router.latestLocation.pathname).toBe(expectedLocation)
+
+ expect(paramsToValidate.textContent).toEqual(JSON.stringify(params))
},
)
})
diff --git a/packages/react-router/tests/navigate.test.tsx b/packages/react-router/tests/navigate.test.tsx
index 574c9fe21fc..1e520c26211 100644
--- a/packages/react-router/tests/navigate.test.tsx
+++ b/packages/react-router/tests/navigate.test.tsx
@@ -1,4 +1,4 @@
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, describe, expect, it, test, vi } from 'vitest'
import { trailingSlashOptions } from '@tanstack/router-core'
import {
@@ -1293,3 +1293,79 @@ describe('splat routes with empty splat', () => {
},
)
})
+
+describe('encoded and unicode paths', () => {
+ const testCases = [
+ {
+ name: 'with prefix',
+ path: '/foo/prefix@대{$}',
+ expectedPath:
+ '/foo/prefix@%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]',
+ expectedLocation: '/foo/prefix@대test[s%5C/.%5C/parameter%25!🚀@]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with suffix',
+ path: '/foo/{$}대suffix@',
+ expectedPath:
+ '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]%EB%8C%80suffix@',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀@]대suffix@',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with wildcard',
+ path: '/foo/$',
+ expectedPath: '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀]',
+ '*': 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ // '/' is left as is with splat params but encoded with normal params
+ {
+ name: 'with path param',
+ path: `/foo/$id`,
+ expectedPath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
+ params: {
+ id: 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ ]
+
+ test.each(testCases)(
+ 'should handle encoded, decoded paths with unicode characters correctly - $name',
+ async ({ path, expectedPath, expectedLocation, params }) => {
+ const rootRoute = createRootRoute()
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ })
+
+ const pathRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path,
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, pathRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ await router.load()
+ await router.navigate({ to: path, params })
+ await router.invalidate()
+
+ expect(router.state.location.href).toBe(expectedPath)
+ expect(router.state.location.pathname).toBe(expectedLocation)
+ },
+ )
+})
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index 4f85a4688e2..cd566e804ff 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -48,6 +48,22 @@ afterEach(() => {
const mockFn1 = vi.fn()
+const URISyntaxCharacters = [
+ [';', '%3B'],
+ [',', '%2C'],
+ ['/', '%2F'],
+ ['?', '%3F'],
+ [':', '%3A'],
+ ['@', '%40'],
+ ['&', '%26'],
+ ['=', '%3D'],
+ ['+', '%2B'],
+ ['$', '%24'],
+ ['#', '%23'],
+ ['\\', '%5C'],
+ ['%', '%25'],
+] as const
+
export function validateSearchParams<
TExpected extends Partial>,
>(expected: TExpected, router: AnyRouter) {
@@ -326,58 +342,125 @@ function createTestRouter(
}
}
-describe('encoding: URL param segment for /posts/$slug', () => {
- it('state.location.pathname, should have the params.slug value of "tanner"', async () => {
+describe('encoding: path params', () => {
+ it('no decoding/encoding of path, url and href required', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
})
await act(() => router.load())
+ expect(router.state.location.url.endsWith('/posts/tanner')).toBe(true)
+ expect(router.state.location.href).toBe('/posts/tanner')
expect(router.state.location.pathname).toBe('/posts/tanner')
})
- it('state.location.pathname, should have the params.slug value of "🚀"', async () => {
+ it('should encode the url and href with unicode param', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/posts/🚀'] }),
})
await act(() => router.load())
- expect(router.state.location.pathname).toBe('/posts/%F0%9F%9A%80')
+ expect(router.state.location.url.endsWith('/posts/%F0%9F%9A%80')).toBe(true)
+ expect(router.state.location.href).toBe('/posts/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/posts/🚀')
})
- it('state.location.pathname, should have the params.slug value of "100%25"', async () => {
+ // the param that is passed in should be the param that is returned
+ it('should treat encoded params as decoded value when encoded params is specified', async () => {
const { router } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/posts/100%25'] }),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
})
await act(() => router.load())
+ await act(() =>
+ router.navigate({
+ to: '/posts/$slug',
+ params: { slug: '100%25' },
+ }),
+ )
- expect(router.state.location.pathname).toBe('/posts/100%25')
+ expect(router.state.location.url.endsWith('/posts/100%2525')).toBe(true)
+ expect(router.state.location.href).toBe('/posts/100%2525')
+ expect(router.state.location.pathname).toBe('/posts/100%2525')
})
- it('state.location.pathname, should have the params.slug value of "100%26"', async () => {
- const { router } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/posts/100%26'] }),
- })
+ describe('pathname and URI syntax characters', () => {
+ it.each(URISyntaxCharacters)(
+ 'pathname should encode $0',
+ async (character, encodedValue) => {
+ const { router } = createTestRouter({
+ history: createMemoryHistory({
+ initialEntries: [`/`],
+ }),
+ })
- await act(() => router.load())
+ await act(() => router.load())
+
+ await act(() =>
+ router.navigate({
+ to: '/posts/$slug',
+ params: { slug: `${character}jane%` },
+ }),
+ )
+
+ expect(
+ router.state.location.url.endsWith(`/posts/${encodedValue}jane%25`),
+ ).toBe(true)
+ expect(router.state.location.href).toBe(`/posts/${encodedValue}jane%25`)
+ expect(router.state.location.pathname).toBe(
+ `/posts/${encodedValue}jane%25`,
+ )
+ },
+ )
- expect(router.state.location.pathname).toBe('/posts/100%26')
+ it.each(
+ URISyntaxCharacters.filter((c) => {
+ const character = c[0]
+
+ const strictlyNotAllowedCharacters = ['/', '?', '\\', '%', '#']
+
+ return !strictlyNotAllowedCharacters.includes(character)
+ }),
+ )('pathname should not encode $0 when allowed', async (character, _) => {
+ const { router } = createTestRouter({
+ history: createMemoryHistory({
+ initialEntries: [`/`],
+ }),
+ pathParamsAllowedCharacters: [character] as any,
+ })
+
+ await act(() => router.load())
+
+ await act(() =>
+ router.navigate({
+ to: '/posts/$slug',
+ params: { slug: `${character}jane%` },
+ }),
+ )
+
+ expect(
+ router.state.location.url.endsWith(`/posts/${character}jane%25`),
+ ).toBe(true)
+ expect(router.state.location.href).toBe(`/posts/${character}jane%25`)
+ expect(router.state.location.pathname).toBe(`/posts/${character}jane%25`)
+ })
})
- it('state.location.pathname, should have the params.slug value of "%F0%9F%9A%80"', async () => {
+ it('pathname should decode encoded unicode param', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/posts/%F0%9F%9A%80'] }),
})
await act(() => router.load())
- expect(router.state.location.pathname).toBe('/posts/%F0%9F%9A%80')
+ expect(router.state.location.url.endsWith('/posts/%F0%9F%9A%80')).toBe(true)
+ expect(router.state.location.href).toBe('/posts/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/posts/🚀')
})
- it('state.location.pathname, should have the params.slug value of "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
+ it('pathname params should decode combination of encoded characters', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({
initialEntries: [
@@ -388,12 +471,20 @@ describe('encoding: URL param segment for /posts/$slug', () => {
await act(() => router.load())
- expect(router.state.location.pathname).toBe(
+ expect(
+ router.state.location.url.endsWith(
+ '/posts/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
+ ),
+ ).toBe(true)
+ expect(router.state.location.href).toBe(
'/posts/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
)
+ expect(router.state.location.pathname).toBe(
+ '/posts/framework%2Freact%2Fguide%2Ffile-based-routing tanstack',
+ )
})
- it('params.slug for the matched route, should be "tanner"', async () => {
+ it('path params no encoding/decoding required', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/posts/tanner'] }),
})
@@ -411,7 +502,7 @@ describe('encoding: URL param segment for /posts/$slug', () => {
expect((match.params as unknown as any).slug).toBe('tanner')
})
- it('params.slug for the matched route, should be "🚀"', async () => {
+ it('path params should keep unicode characters', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/posts/🚀'] }),
})
@@ -429,7 +520,7 @@ describe('encoding: URL param segment for /posts/$slug', () => {
expect((match.params as unknown as any).slug).toBe('🚀')
})
- it('params.slug for the matched route, should be "🚀" instead of it being "%F0%9F%9A%80"', async () => {
+ it('path params should decode encoded unicode characters', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/posts/%F0%9F%9A%80'] }),
})
@@ -447,31 +538,18 @@ describe('encoding: URL param segment for /posts/$slug', () => {
expect((match.params as unknown as any).slug).toBe('🚀')
})
- it('params.slug for the matched route, should be "100%"', async () => {
+ // the param that is passed in should be the param that is returned
+ it('path params should not decode encoded characters when passed as param', async () => {
const { router, routes } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/posts/100%25'] }),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
})
await act(() => router.load())
- const match = router.state.matches.find(
- (r) => r.routeId === routes.postIdRoute.id,
+ await act(() =>
+ router.navigate({ to: '/posts/$slug', params: { slug: '100%25' } }),
)
- if (!match) {
- throw new Error('No match found')
- }
-
- expect((match.params as unknown as any).slug).toBe('100%')
- })
-
- it('params.slug for the matched route, should be "100&"', async () => {
- const { router, routes } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/posts/100%26'] }),
- })
-
- await act(() => router.load())
-
const match = router.state.matches.find(
(r) => r.routeId === routes.postIdRoute.id,
)
@@ -480,46 +558,35 @@ describe('encoding: URL param segment for /posts/$slug', () => {
throw new Error('No match found')
}
- expect((match.params as unknown as any).slug).toBe('100&')
+ expect((match.params as unknown as any).slug).toBe('100%25')
})
- it('params.slug for the matched route, should be "100%100"', async () => {
- const { router, routes } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/posts/100%25100'] }),
- })
-
- await act(() => router.load())
-
- const match = router.state.matches.find(
- (r) => r.routeId === routes.postIdRoute.id,
- )
-
- if (!match) {
- throw new Error('No match found')
- }
+ describe('path params should decode encoded URI syntax characters', () => {
+ it.each(URISyntaxCharacters)(
+ '$1 => $0 - should be decoded',
+ async (character, encodedValue) => {
+ const { router, routes } = createTestRouter({
+ history: createMemoryHistory({
+ initialEntries: [`/posts/100${encodedValue}100`],
+ }),
+ })
- expect((match.params as unknown as any).slug).toBe('100%100')
- })
+ await act(() => router.load())
- it('params.slug for the matched route, should be "100&100"', async () => {
- const { router, routes } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/posts/100%26100'] }),
- })
+ const match = router.state.matches.find(
+ (r) => r.routeId === routes.postIdRoute.id,
+ )
- await act(() => router.load())
+ if (!match) {
+ throw new Error('No match found')
+ }
- const match = router.state.matches.find(
- (r) => r.routeId === routes.postIdRoute.id,
+ expect((match.params as unknown as any).slug).toBe(`100${character}100`)
+ },
)
-
- if (!match) {
- throw new Error('No match found')
- }
-
- expect((match.params as unknown as any).slug).toBe('100&100')
})
- it('params.slug for the matched route, should be "framework/react/guide/file-based-routing tanstack" instead of it being "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
+ it('path params should decode combination of encoded characters', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({
initialEntries: [
@@ -542,91 +609,67 @@ describe('encoding: URL param segment for /posts/$slug', () => {
'framework/react/guide/file-based-routing tanstack',
)
})
-
- it('params.slug should be encoded in the final URL', async () => {
- const { router } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/'] }),
- })
-
- await router.load()
- render()
-
- await act(() =>
- router.navigate({ to: '/posts/$slug', params: { slug: '@jane' } }),
- )
-
- expect(router.state.location.pathname).toBe('/posts/%40jane')
- })
-
- it('params.slug should be encoded in the final URL except characters in pathParamsAllowedCharacters', async () => {
- const { router } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/'] }),
- pathParamsAllowedCharacters: ['@'],
- })
-
- await router.load()
- render()
-
- await act(() =>
- router.navigate({ to: '/posts/$slug', params: { slug: '@jane' } }),
- )
-
- expect(router.state.location.pathname).toBe('/posts/@jane')
- })
})
-describe('encoding: URL splat segment for /$', () => {
- it('state.location.pathname, should have the params._splat value of "tanner"', async () => {
+describe('encoding/decoding: wildcard routes/params', () => {
+ it('no decoding/encoding of path, url and href required', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/tanner'] }),
})
await router.load()
+ expect(router.state.location.url.endsWith('/tanner')).toBe(true)
+ expect(router.state.location.href).toBe('/tanner')
expect(router.state.location.pathname).toBe('/tanner')
})
- it('state.location.pathname, should have the params._splat value of "🚀"', async () => {
+ it('should encode the url and href with unicode param', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/🚀'] }),
})
await router.load()
- expect(router.state.location.pathname).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.url.endsWith('/%F0%9F%9A%80')).toBe(true)
+ expect(router.state.location.href).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/🚀')
})
- it('state.location.pathname, should have the params._splat value of "100%25"', async () => {
- const { router } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/100%25'] }),
- })
-
- await router.load()
-
- expect(router.state.location.pathname).toBe('/100%25')
- })
-
- it('state.location.pathname, should have the params._splat value of "100%26"', async () => {
- const { router } = createTestRouter({
- history: createMemoryHistory({ initialEntries: ['/100%26'] }),
- })
+ describe('pathname and URI syntax characters', () => {
+ it.each(URISyntaxCharacters)(
+ '$1 should not be decoded for pathname',
+ async (character, encodedValue) => {
+ const { router } = createTestRouter({
+ history: createMemoryHistory({
+ initialEntries: [`/100${encodedValue}100`],
+ }),
+ })
- await router.load()
+ await router.load()
- expect(router.state.location.pathname).toBe('/100%26')
+ expect(
+ router.state.location.url.endsWith(`/100${encodedValue}100`),
+ ).toBe(true)
+ expect(router.state.location.href).toBe(`/100${encodedValue}100`)
+ expect(router.state.location.pathname).toBe(`/100${encodedValue}100`)
+ },
+ )
})
- it('state.location.pathname, should have the params._splat value of "%F0%9F%9A%80"', async () => {
+ it('pathname should decode encoded unicode path', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/%F0%9F%9A%80'] }),
})
await router.load()
- expect(router.state.location.pathname).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.url.endsWith('/%F0%9F%9A%80')).toBe(true)
+ expect(router.state.location.href).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/🚀')
})
- it('state.location.pathname, should have the params._splat value of "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
+ it('decode only non URI syntax characters in path', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({
initialEntries: [
@@ -637,12 +680,20 @@ describe('encoding: URL splat segment for /$', () => {
await router.load()
+ expect(
+ router.state.location.url.endsWith(
+ '/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
+ ),
+ ).toBe(true)
expect(router.state.location.href).toBe(
'/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
)
+ expect(router.state.location.pathname).toBe(
+ '/framework%2Freact%2Fguide%2Ffile-based-routing tanstack',
+ )
})
- it('state.location.pathname, should have the params._splat value of "framework/react/guide/file-based-routing tanstack"', async () => {
+ it('"/" should not be encoded in splat param paths', async () => {
const { router } = createTestRouter({
history: createMemoryHistory({
initialEntries: ['/framework/react/guide/file-based-routing tanstack'],
@@ -651,12 +702,22 @@ describe('encoding: URL splat segment for /$', () => {
await router.load()
+ expect(
+ router.state.location.url.endsWith(
+ '/framework/react/guide/file-based-routing%20tanstack',
+ ),
+ ).toBe(true)
+
expect(router.state.location.href).toBe(
'/framework/react/guide/file-based-routing%20tanstack',
)
+
+ expect(router.state.location.pathname).toBe(
+ '/framework/react/guide/file-based-routing tanstack',
+ )
})
- it('params._splat for the matched route, should be "tanner"', async () => {
+ it('no encoding/decoding of param required', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/tanner'] }),
})
@@ -674,7 +735,7 @@ describe('encoding: URL splat segment for /$', () => {
expect((match.params as unknown as any)._splat).toBe('tanner')
})
- it('params._splat for the matched route, should be "🚀"', async () => {
+ it('param should keep decoded unicode param', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/🚀'] }),
})
@@ -692,7 +753,7 @@ describe('encoding: URL splat segment for /$', () => {
expect((match.params as unknown as any)._splat).toBe('🚀')
})
- it('params._splat for the matched route, should be "🚀" instead of it being "%F0%9F%9A%80"', async () => {
+ it('param should be decoded for encoded unicode param', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({ initialEntries: ['/%F0%9F%9A%80'] }),
})
@@ -710,7 +771,7 @@ describe('encoding: URL splat segment for /$', () => {
expect((match.params as unknown as any)._splat).toBe('🚀')
})
- it('params._splat for the matched route, should be "framework/react/guide/file-based-routing tanstack"', async () => {
+ it('"/" should not be encoded in splat params', async () => {
const { router, routes } = createTestRouter({
history: createMemoryHistory({
initialEntries: ['/framework/react/guide/file-based-routing tanstack'],
@@ -733,65 +794,58 @@ describe('encoding: URL splat segment for /$', () => {
})
})
-describe('encoding: URL path segment', () => {
+describe('encoding/decoding: URL path segment', () => {
it.each([
{
+ test: 'should decode the pathname',
input: '/path-segment/%C3%A9',
- output: '/path-segment/%C3%A9',
- },
- {
- input: '/path-segment/é',
- output: '/path-segment/%C3%A9',
- type: 'not encoded',
+ path: '/path-segment/é',
+ url: '/path-segment/%C3%A9',
},
{
+ test: 'should not decode excluded characters',
input: '/path-segment/100%25', // `%25` = `%`
- output: '/path-segment/100%25',
- type: 'not encoded',
+ path: '/path-segment/100%25',
+ url: '/path-segment/100%25',
},
{
+ test: 'should not decode multiple excluded characters',
input: '/path-segment/100%25%25',
- output: '/path-segment/100%25%25',
- type: 'not encoded',
+ path: '/path-segment/100%25%25',
+ url: '/path-segment/100%25%25',
},
{
+ test: 'should not decode characters that are part of the URI syntax',
input: '/path-segment/100%26', // `%26` = `&`
- output: '/path-segment/100%26',
- type: 'not encoded',
+ path: '/path-segment/100%26',
+ url: '/path-segment/100%26',
},
{
+ test: 'should decode unicode characters',
input: '/path-segment/%F0%9F%9A%80',
- output: '/path-segment/%F0%9F%9A%80',
+ path: '/path-segment/🚀',
+ url: '/path-segment/%F0%9F%9A%80',
},
{
- input: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
- output: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
+ test: 'should encode unicode characters in the url and href',
+ input: '/path-segment/🚀é',
+ path: '/path-segment/🚀é',
+ url: '/path-segment/%F0%9F%9A%80%C3%A9',
},
{
- input: '/path-segment/%25%F0%9F%9A%80to%2Fthe%2Fmoon',
- output: '/path-segment/%25%F0%9F%9A%80to%2Fthe%2Fmoon',
- },
- {
- input: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25',
- output: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25',
+ test: 'should encode unicode characters in the url and href and maintain already encoded characters',
+ input: '/path-segment/🚀to%2Fthe%2Fmoon',
+ path: '/path-segment/🚀to%2Fthe%2Fmoon',
+ url: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
},
{
+ test: 'should decode/encode combination of excluded, URI syntax and unicode characters correctly in the path, url and href',
input:
- '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25%F0%9F%9A%80to%2Fthe%2Fmoon',
- output:
- '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25%F0%9F%9A%80to%2Fthe%2Fmoon',
- },
- {
- input: '/path-segment/🚀',
- output: '/path-segment/%F0%9F%9A%80',
- type: 'not encoded',
- },
- {
- input: '/path-segment/🚀to%2Fthe%2Fmoon',
- output: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
- type: 'not encoded',
+ '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25%F0%9F%9A%80to%2Fthe%2Fmoon%25',
+ path: '/path-segment/🚀to%2Fthe%2Fmoon%25🚀to%2Fthe%2Fmoon%25',
+ url: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25%F0%9F%9A%80to%2Fthe%2Fmoon%25',
},
- ])('should resolve $input to $output', async ({ input, output }) => {
+ ])('$test', async ({ input, path, url }) => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: [input] }),
})
@@ -799,7 +853,9 @@ describe('encoding: URL path segment', () => {
render()
await act(() => router.load())
- expect(new URL(router.state.location.url).pathname).toBe(output)
+ expect(router.state.location.pathname).toBe(path)
+ expect(router.state.location.href).toBe(url)
+ expect(new URL(router.state.location.url).pathname).toBe(url)
})
})
diff --git a/packages/react-router/tests/useNavigate.test.tsx b/packages/react-router/tests/useNavigate.test.tsx
index 539ab312c69..e3475c2e274 100644
--- a/packages/react-router/tests/useNavigate.test.tsx
+++ b/packages/react-router/tests/useNavigate.test.tsx
@@ -2660,3 +2660,118 @@ describe('splat routes with empty splat', () => {
},
)
})
+
+describe('encoded and unicode paths', () => {
+ const testCases = [
+ {
+ name: 'with prefix',
+ path: '/foo/prefix@대{$}',
+ expectedPath:
+ '/foo/prefix@%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]',
+ expectedLocation: '/foo/prefix@대test[s%5C/.%5C/parameter%25!🚀@]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with suffix',
+ path: '/foo/{$}대suffix@',
+ expectedPath:
+ '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]%EB%8C%80suffix@',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀@]대suffix@',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with wildcard',
+ path: '/foo/$',
+ expectedPath: '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀]',
+ '*': 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ // '/' is left as is with splat params but encoded with normal params
+ {
+ name: 'with path param',
+ path: `/foo/$id`,
+ expectedPath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
+ params: {
+ id: 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ ]
+
+ test.each(testCases)(
+ 'should handle encoded, decoded paths with unicode characters correctly - $name',
+ async ({ path, expectedPath, expectedLocation, params }) => {
+ const rootRoute = createRootRoute()
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+ })
+
+ function IndexComponent() {
+ const navigate = useNavigate()
+
+ return (
+ <>
+ Index Route
+
+ >
+ )
+ }
+
+ const pathRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path,
+ component: PathRouteComponent,
+ })
+
+ function PathRouteComponent() {
+ const params = pathRoute.useParams()
+ return (
+
+
Path Route
+
+ params:{' '}
+
+ {JSON.stringify(params)}
+
+
+
+ )
+ }
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, pathRoute]),
+ history,
+ })
+
+ render()
+
+ const link = await screen.findByTestId('btn-to-path')
+
+ await act(() => fireEvent.click(link))
+
+ const paramsToValidate = await screen.findByTestId('params-to-validate')
+
+ expect(window.location.pathname).toBe(expectedPath)
+ expect(router.latestLocation.pathname).toBe(expectedLocation)
+
+ expect(paramsToValidate.textContent).toEqual(JSON.stringify(params))
+ },
+ )
+})
diff --git a/packages/router-core/src/index.ts b/packages/router-core/src/index.ts
index 98baed5f55b..b86a1ed67f5 100644
--- a/packages/router-core/src/index.ts
+++ b/packages/router-core/src/index.ts
@@ -278,6 +278,7 @@ export {
deepEqual,
createControlledPromise,
isModuleNotFoundError,
+ decodePath,
} from './utils'
export type {
NoInfer,
diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts
index eac8940ae5e..6b97f944bfa 100644
--- a/packages/router-core/src/path.ts
+++ b/packages/router-core/src/path.ts
@@ -1,4 +1,4 @@
-import { decodePathSegment, last } from './utils'
+import { last } from './utils'
import type { LRUCache } from './lru-cache'
import type { MatchLocation } from './RouterProvider'
import type { AnyPathParams } from './route'
@@ -358,7 +358,7 @@ function baseParsePathname(pathname: string): ReadonlyArray {
// Handle regular pathname segment
return {
type: SEGMENT_TYPE_PATHNAME,
- value: decodePathSegment(part),
+ value: part,
}
}),
)
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index bd4d22860d3..b6a700e8e6d 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -2,7 +2,7 @@ import { Store, batch } from '@tanstack/store'
import { createBrowserHistory, parseHref } from '@tanstack/history'
import {
createControlledPromise,
- decodePathSegment,
+ decodePath,
deepEqual,
findLast,
functionalUpdate,
@@ -1173,7 +1173,7 @@ export class RouterCore<
href: fullPath,
publicHref: href,
url: url.href,
- pathname,
+ pathname: decodePath(pathname),
searchStr,
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
hash: hash.split('#').reverse()[0] ?? '',
@@ -1671,7 +1671,7 @@ export class RouterCore<
}
}
- const nextPathname = decodePathSegment(
+ const nextPathname = decodePath(
interpolatePath({
// Use the original template path for interpolation
// This preserves the original parameter syntax including optional parameters
diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts
index 2012d259166..ccc4654dbe4 100644
--- a/packages/router-core/src/utils.ts
+++ b/packages/router-core/src/utils.ts
@@ -493,31 +493,53 @@ const DECODE_IGNORE_LIST = Array.from(
new Map([
['%', '%25'],
['\\', '%5C'],
- ['/', '%2F'],
- [';', '%3B'],
- [':', '%3A'],
- ['@', '%40'],
- ['&', '%26'],
- ['=', '%3D'],
- ['+', '%2B'],
- ['$', '%24'],
- [',', '%2C'],
]).values(),
)
-export function decodePathSegment(
+export function decodePath(
part: string,
decodeIgnore: Array = DECODE_IGNORE_LIST,
- startIndex = 0,
): string {
- function decode(part: string): string {
+ function splitAndDecode(
+ part: string,
+ decodeIgnore: Array,
+ startIndex = 0,
+ ): string {
+ // decode the path / path segment by splitting it into parts defined by the ignore list.
+ // once these pieces have been decoded, join them back together to form the final decoded path segment with the ignored character in place.
+ // we walk through the ignore list linearly, breaking the segment up into pieces and decoding each piece individually.
+ // use index traversal to avoid making unnecessary copies of the array.
+ for (let i = startIndex; i < decodeIgnore.length; i++) {
+ const char = decodeIgnore[i]!.toUpperCase()
+
+ // check if the part includes the current ignore character
+ // if it doesn't continue to the next ignore character
+ if (part.includes(char)) {
+ // split the part into pieces that needs to be checked and decoded
+ const partsToDecode = part.split(char)
+ const partsToJoin: Array = []
+
+ // now check and decode each piece individually taking into consideration the remaining ignored characters.
+ // since we are walking through the list linearly, we only need to consider ignore items not yet traversed.
+ for (const partToDecode of partsToDecode) {
+ // once we have traversed the entire ignore list, each decoded part is returned.
+ partsToJoin.push(splitAndDecode(partToDecode, decodeIgnore, i + 1))
+ }
+
+ // and join them back together to form the final decoded path segment with the ignored character in place.
+ return partsToJoin.join(char)
+ }
+ }
+
+ // once we have reached the end of the ignore list, we start walking back returning each decoded part.
+ // should there be no matching characters, the path segment as a whole will be decoded.
try {
- return decodeURIComponent(part)
+ return decodeURI(part)
} catch {
// if the decoding fails, try to decode the various parts leaving the malformed tags in place
- return part.replaceAll(/%[0-9A-Fa-f]{2}/g, (match) => {
+ return part.replaceAll(/%[0-9A-F]{2}/g, (match) => {
try {
- return decodeURIComponent(match)
+ return decodeURI(match)
} catch {
return match
}
@@ -526,35 +548,12 @@ export function decodePathSegment(
}
// if the path segment does not contain any encoded uri components return the path as is
- if (part === '' || !part.match(/%[0-9A-Fa-f]{2}/g)) return part
-
- // decode the path / path segment by splitting it into parts defined by the ignore list.
- // once these pieces have been decoded, join them back together to form the final decoded path segment with the ignored character in place.
- // we walk through the ignore list linearly, breaking the segment up into pieces and decoding each piece individually.
- // use index traversal to avoid making unnecessary copies of the array.
- for (let i = startIndex; i < decodeIgnore.length; i++) {
- const char = decodeIgnore[i]
-
- // check if the part includes the current ignore character
- // if it doesn't continue to the next ignore character
- if (char && part.includes(char)) {
- // split the part into pieces that needs to be checked and decoded
- const partsToDecode = part.split(char)
- const partsToJoin: Array = []
-
- // now check and decode each piece individually taking into consideration the remaining ignored characters.
- // since we are walking through the list linearly, we only need to consider ignore items not yet traversed.
- for (const partToDecode of partsToDecode) {
- // once we have traversed the entire ignore list, each decoded part is returned.
- partsToJoin.push(decodePathSegment(partToDecode, decodeIgnore, i + 1))
- }
+ if (part === '' || !/%[0-9A-Fa-f]{2}/g.test(part)) return part
- // and join them back together to form the final decoded path segment with the ignored character in place.
- return partsToJoin.join(char)
- }
- }
+ // ensure all encoded characters are uppercase
+ const normalizedPart = part.replaceAll(/%[0-9a-f]{2}/g, (match) =>
+ match.toUpperCase(),
+ )
- // once we have reached the end of the ignore list, we start walking back returning each decoded part.
- // should there be no matching characters, the path segment as a whole will be decoded.
- return decode(part)
+ return splitAndDecode(normalizedPart, decodeIgnore)
}
diff --git a/packages/router-core/tests/path.test.ts b/packages/router-core/tests/path.test.ts
index a98f6d5f433..20eefe3d6cc 100644
--- a/packages/router-core/tests/path.test.ts
+++ b/packages/router-core/tests/path.test.ts
@@ -832,14 +832,6 @@ describe('parsePathname', () => {
{ type: SEGMENT_TYPE_WILDCARD, value: '$' },
],
},
- {
- name: 'should preserve backslashes and percentage signs in path parameters',
- to: '/%25%D1%88%D0%B5%5C%D0%BB%D0%BB%D1%8B',
- expected: [
- { type: SEGMENT_TYPE_PATHNAME, value: '/' },
- { type: SEGMENT_TYPE_PATHNAME, value: '%25ше%5Cллы' },
- ],
- },
] satisfies ParsePathnameTestScheme)('$name', ({ to, expected }) => {
const result = parsePathname(to)
expect(result).toEqual(expected)
diff --git a/packages/router-core/tests/utils.test.ts b/packages/router-core/tests/utils.test.ts
index d00a17b4d52..4dec592a57c 100644
--- a/packages/router-core/tests/utils.test.ts
+++ b/packages/router-core/tests/utils.test.ts
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it } from 'vitest'
import {
- decodePathSegment,
+ decodePath,
deepEqual,
isPlainArray,
replaceEqualDeep,
@@ -498,14 +498,14 @@ describe('deepEqual', () => {
})
})
-describe('decodePathSegment', () => {
+describe('decodePath', () => {
it('should decode a path segment with no ignored items existing', () => {
const itemsToCheck = ['%25', '%5C']
const stringToCheck =
'https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B'
const expectedResult = 'https://mozilla.org/?x=шеллы'
- const result = decodePathSegment(stringToCheck, itemsToCheck)
+ const result = decodePath(stringToCheck, itemsToCheck)
expect(result).toBe(expectedResult)
})
@@ -516,7 +516,7 @@ describe('decodePathSegment', () => {
'https://mozilla.org/?x=%25%D1%88%D0%B5%5C%D0%BB%D0%BB%D1%8B'
const expectedResult = 'https://mozilla.org/?x=%25ше\\ллы'
- const result = decodePathSegment(stringToCheck, itemsToCheck)
+ const result = decodePath(stringToCheck, itemsToCheck)
expect(result).toBe(expectedResult)
})
@@ -527,14 +527,14 @@ describe('decodePathSegment', () => {
'https://mozilla.org/?x=%25%D1%88%D0%B5%D0%BB%D0%BB%D1%8B'
let expectedResult = 'https://mozilla.org/?x=%25шеллы'
- let result = decodePathSegment(stringToCheck, itemsToCheck)
+ let result = decodePath(stringToCheck, itemsToCheck)
expect(result).toBe(expectedResult)
stringToCheck = 'https://mozilla.org/?x=%D1%88%D0%B5%5C%D0%BB%D0%BB%D1%8B'
expectedResult = 'https://mozilla.org/?x=ше%5Cллы'
- result = decodePathSegment(stringToCheck, itemsToCheck)
+ result = decodePath(stringToCheck, itemsToCheck)
expect(result).toBe(expectedResult)
})
@@ -545,14 +545,14 @@ describe('decodePathSegment', () => {
'https://mozilla.org/?x=%5C%D1%88%D0%B5%D0%BB%D0%BB%D1%8B'
let expectedResult = 'https://mozilla.org/?x=%5Cшеллы'
- let result = decodePathSegment(stringToCheck, itemsToCheck)
+ let result = decodePath(stringToCheck, itemsToCheck)
expect(result).toBe(expectedResult)
stringToCheck = 'https://mozilla.org/?x=%D1%88%D0%B5%5C%D0%BB%D0%BB%D1%8B'
expectedResult = 'https://mozilla.org/?x=ше%5Cллы'
- result = decodePathSegment(stringToCheck, itemsToCheck)
+ result = decodePath(stringToCheck, itemsToCheck)
expect(result).toBe(expectedResult)
})
@@ -563,7 +563,7 @@ describe('decodePathSegment', () => {
'https://mozilla.org/?x=%25%D1%88%D0%B5%5C%D0%BB%D0%BB%D1%8B'
const expectedResult = 'https://mozilla.org/?x=%25ше%5Cллы'
- const result = decodePathSegment(stringToCheck, itemsToCheck)
+ const result = decodePath(stringToCheck, itemsToCheck)
expect(result).toBe(expectedResult)
})
@@ -573,7 +573,7 @@ describe('decodePathSegment', () => {
'https://mozilla.org/?x=%25%D1%88%D0%B5%5C%D0%BB%D0%BB%D1%8B%2F'
const expectedResult = 'https://mozilla.org/?x=%25ше%5Cллы%2F'
- const result = decodePathSegment(stringToCheck)
+ const result = decodePath(stringToCheck)
expect(result).toBe(expectedResult)
})
@@ -581,22 +581,35 @@ describe('decodePathSegment', () => {
it('should handle malformed percent-encodings gracefully', () => {
const stringToCheck = 'path%ZZ%D1%88test%5C%C3%A9'
// Malformed sequences should remain as-is, valid ones decoded
- const result = decodePathSegment(stringToCheck)
+ const result = decodePath(stringToCheck)
expect(result).toBe(`path%ZZ%D1%88test%5Cé`)
})
it('should return empty string unchanged', () => {
- expect(decodePathSegment('')).toBe('')
+ expect(decodePath('')).toBe('')
})
it('should return strings without encoding unchanged', () => {
const stringToCheck = 'plain-text-path'
- expect(decodePathSegment(stringToCheck)).toBe(stringToCheck)
+ expect(decodePath(stringToCheck)).toBe(stringToCheck)
})
it('should handle consecutive ignored characters', () => {
const stringToCheck = 'test%25%25end'
const expectedResult = 'test%25%25end'
- expect(decodePathSegment(stringToCheck)).toBe(expectedResult)
+ expect(decodePath(stringToCheck)).toBe(expectedResult)
+ })
+
+ it('should handle multiple ignored items of the same type with varying case', () => {
+ const stringToCheck = '/params-ps/named/foo%2Fabc/c%2Fh'
+ const expectedResult = '/params-ps/named/foo%2Fabc/c%2Fh'
+ expect(decodePath(stringToCheck)).toBe(expectedResult)
+
+ const stringToCheckWithLowerCase = '/params-ps/named/foo%2Fabc/c%5C%2f%5cAh'
+ const expectedResultWithLowerCase =
+ '/params-ps/named/foo%2Fabc/c%5C%2F%5CAh'
+ expect(decodePath(stringToCheckWithLowerCase)).toBe(
+ expectedResultWithLowerCase,
+ )
})
})
diff --git a/packages/solid-router/tests/link.test.tsx b/packages/solid-router/tests/link.test.tsx
index 8d6efa6812c..8d9f36374d8 100644
--- a/packages/solid-router/tests/link.test.tsx
+++ b/packages/solid-router/tests/link.test.tsx
@@ -6494,31 +6494,31 @@ describe('encoded and unicode paths', () => {
const testCases = [
{
name: 'with prefix',
- path: '/foo/prefix대{$}',
- browsePath: '/foo/prefix대test[s%5C/.%5C/parameter%25!🚀]',
+ path: '/foo/prefix@대{$}',
expectedPath:
- '/foo/prefix%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ '/foo/prefix@%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]',
+ expectedLocation: '/foo/prefix@대test[s%5C/.%5C/parameter%25!🚀@]',
params: {
- _splat: 'test[s\\/.\\/parameter%!🚀]',
- '*': 'test[s\\/.\\/parameter%!🚀]',
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
},
},
{
name: 'with suffix',
- path: '/foo/{$}대suffix',
- browsePath: '/foo/test[s%5C/.%5C/parameter%25!🚀]대suffix',
+ path: '/foo/{$}대suffix@',
expectedPath:
- '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]%EB%8C%80suffix',
+ '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]%EB%8C%80suffix@',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀@]대suffix@',
params: {
- _splat: 'test[s\\/.\\/parameter%!🚀]',
- '*': 'test[s\\/.\\/parameter%!🚀]',
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
},
},
{
name: 'with wildcard',
path: '/foo/$',
- browsePath: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
expectedPath: '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
params: {
_splat: 'test[s\\/.\\/parameter%!🚀]',
'*': 'test[s\\/.\\/parameter%!🚀]',
@@ -6528,8 +6528,8 @@ describe('encoded and unicode paths', () => {
{
name: 'with path param',
path: `/foo/$id`,
- browsePath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
expectedPath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
params: {
id: 'test[s\\/.\\/parameter%!🚀]',
},
@@ -6538,16 +6538,7 @@ describe('encoded and unicode paths', () => {
test.each(testCases)(
'should handle encoded, decoded paths with unicode characters correctly - $name',
- async ({ path, browsePath, expectedPath, params }) => {
- async function validate() {
- const paramsToValidate = await screen.findByTestId('params-to-validate')
-
- await waitFor(() => expect(window.location.pathname).toBe(expectedPath))
- await waitFor(() =>
- expect(paramsToValidate.textContent).toEqual(JSON.stringify(params)),
- )
- }
-
+ async ({ path, expectedPath, expectedLocation, params }) => {
const rootRoute = createRootRoute()
const indexRoute = createRoute({
@@ -6593,19 +6584,18 @@ describe('encoded and unicode paths', () => {
render(() => )
- history.push(browsePath)
-
- await validate()
-
- history.push('/')
-
const link = await screen.findByTestId('link-to-path')
expect(link.getAttribute('href')).toBe(expectedPath)
fireEvent.click(link)
- await validate()
+ const paramsToValidate = await screen.findByTestId('params-to-validate')
+
+ expect(window.location.pathname).toBe(expectedPath)
+ expect(router.latestLocation.pathname).toBe(expectedLocation)
+
+ expect(paramsToValidate.textContent).toEqual(JSON.stringify(params))
},
)
})
diff --git a/packages/solid-router/tests/navigate.test.tsx b/packages/solid-router/tests/navigate.test.tsx
index 7fa8fa26d0f..8c5ac5ade46 100644
--- a/packages/solid-router/tests/navigate.test.tsx
+++ b/packages/solid-router/tests/navigate.test.tsx
@@ -1,4 +1,4 @@
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, describe, expect, it, test, vi } from 'vitest'
import { trailingSlashOptions } from '@tanstack/router-core'
import { waitFor } from '@solidjs/testing-library'
@@ -1263,3 +1263,79 @@ describe('router.navigate navigation using optional path parameters - edge cases
expect(router.state.location.pathname).toBe('/files/prefix.txt')
})
})
+
+describe('encoded and unicode paths', () => {
+ const testCases = [
+ {
+ name: 'with prefix',
+ path: '/foo/prefix@대{$}',
+ expectedPath:
+ '/foo/prefix@%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]',
+ expectedLocation: '/foo/prefix@대test[s%5C/.%5C/parameter%25!🚀@]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with suffix',
+ path: '/foo/{$}대suffix@',
+ expectedPath:
+ '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]%EB%8C%80suffix@',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀@]대suffix@',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with wildcard',
+ path: '/foo/$',
+ expectedPath: '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀]',
+ '*': 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ // '/' is left as is with splat params but encoded with normal params
+ {
+ name: 'with path param',
+ path: `/foo/$id`,
+ expectedPath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
+ params: {
+ id: 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ ]
+
+ test.each(testCases)(
+ 'should handle encoded, decoded paths with unicode characters correctly - $name',
+ async ({ path, expectedPath, expectedLocation, params }) => {
+ const rootRoute = createRootRoute()
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ })
+
+ const pathRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path,
+ })
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, pathRoute]),
+ history: createMemoryHistory({ initialEntries: ['/'] }),
+ })
+
+ await router.load()
+ await router.navigate({ to: path, params })
+ await router.invalidate()
+
+ expect(router.state.location.href).toBe(expectedPath)
+ expect(router.state.location.pathname).toBe(expectedLocation)
+ },
+ )
+})
diff --git a/packages/solid-router/tests/router.test.tsx b/packages/solid-router/tests/router.test.tsx
index fdfcd91e4f1..aa30b2a000a 100644
--- a/packages/solid-router/tests/router.test.tsx
+++ b/packages/solid-router/tests/router.test.tsx
@@ -334,7 +334,7 @@ describe('encoding: URL param segment for /posts/$slug', () => {
await router.load()
- expect(router.state.location.pathname).toBe('/posts/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/posts/🚀')
})
it('state.location.pathname, should have the params.slug value of "100%25"', async () => {
@@ -364,7 +364,7 @@ describe('encoding: URL param segment for /posts/$slug', () => {
await router.load()
- expect(router.state.location.pathname).toBe('/posts/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/posts/🚀')
})
it('state.location.pathname, should have the params.slug value of "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
@@ -378,9 +378,13 @@ describe('encoding: URL param segment for /posts/$slug', () => {
await router.load()
- expect(router.state.location.pathname).toBe(
+ expect(router.state.location.href).toBe(
'/posts/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
)
+
+ expect(router.state.location.pathname).toBe(
+ '/posts/framework%2Freact%2Fguide%2Ffile-based-routing tanstack',
+ )
})
it('params.slug for the matched route, should be "tanner"', async () => {
@@ -579,7 +583,8 @@ describe('encoding: URL splat segment for /$', () => {
await router.load()
- expect(router.state.location.pathname).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.href).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/🚀')
})
it('state.location.pathname, should have the params._splat value of "100%25"', async () => {
@@ -609,7 +614,8 @@ describe('encoding: URL splat segment for /$', () => {
await router.load()
- expect(router.state.location.pathname).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.href).toBe('/%F0%9F%9A%80')
+ expect(router.state.location.pathname).toBe('/🚀')
})
it('state.location.pathname, should have the params._splat value of "framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack"', async () => {
@@ -623,9 +629,12 @@ describe('encoding: URL splat segment for /$', () => {
await router.load()
- expect(router.state.location.pathname).toBe(
+ expect(router.state.location.href).toBe(
'/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
)
+ expect(router.state.location.pathname).toBe(
+ '/framework%2Freact%2Fguide%2Ffile-based-routing tanstack',
+ )
})
it('state.location.pathname, should have the params._splat value of "framework/react/guide/file-based-routing tanstack"', async () => {
@@ -637,9 +646,13 @@ describe('encoding: URL splat segment for /$', () => {
await router.load()
- expect(router.state.location.pathname).toBe(
+ expect(router.state.location.href).toBe(
'/framework/react/guide/file-based-routing%20tanstack',
)
+
+ expect(router.state.location.pathname).toBe(
+ '/framework/react/guide/file-based-routing tanstack',
+ )
})
it('params._splat for the matched route, should be "tanner"', async () => {
@@ -723,55 +736,66 @@ describe('encoding: URL path segment', () => {
it.each([
{
input: '/path-segment/%C3%A9',
- output: '/path-segment/%C3%A9',
+ path: '/path-segment/é',
+ url: '/path-segment/%C3%A9',
},
{
input: '/path-segment/é',
- output: '/path-segment/%C3%A9',
+ path: '/path-segment/é',
+ url: '/path-segment/%C3%A9',
},
{
input: '/path-segment/100%25', // `%25` = `%`
- output: '/path-segment/100%25',
+ path: '/path-segment/100%25',
+ url: '/path-segment/100%25',
},
{
input: '/path-segment/100%25%25',
- output: '/path-segment/100%25%25',
+ path: '/path-segment/100%25%25',
+ url: '/path-segment/100%25%25',
},
{
input: '/path-segment/100%26', // `%26` = `&`
- output: '/path-segment/100%26',
+ path: '/path-segment/100%26',
+ url: '/path-segment/100%26',
},
{
input: '/path-segment/%F0%9F%9A%80',
- output: '/path-segment/%F0%9F%9A%80',
+ path: '/path-segment/🚀',
+ url: '/path-segment/%F0%9F%9A%80',
},
{
input: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
- output: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
+ path: '/path-segment/🚀to%2Fthe%2Fmoon',
+ url: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
},
{
input: '/path-segment/%25%F0%9F%9A%80to%2Fthe%2Fmoon',
- output: '/path-segment/%25%F0%9F%9A%80to%2Fthe%2Fmoon',
+ path: '/path-segment/%25🚀to%2Fthe%2Fmoon',
+ url: '/path-segment/%25%F0%9F%9A%80to%2Fthe%2Fmoon',
},
{
input: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25',
- output: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25',
+ path: '/path-segment/🚀to%2Fthe%2Fmoon%25',
+ url: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25',
},
{
input:
'/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25%F0%9F%9A%80to%2Fthe%2Fmoon',
- output:
- '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25%F0%9F%9A%80to%2Fthe%2Fmoon',
+ path: '/path-segment/🚀to%2Fthe%2Fmoon%25🚀to%2Fthe%2Fmoon',
+ url: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon%25%F0%9F%9A%80to%2Fthe%2Fmoon',
},
{
input: '/path-segment/🚀',
- output: '/path-segment/%F0%9F%9A%80',
+ path: '/path-segment/🚀',
+ url: '/path-segment/%F0%9F%9A%80',
},
{
input: '/path-segment/🚀to%2Fthe%2Fmoon',
- output: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
+ path: '/path-segment/🚀to%2Fthe%2Fmoon',
+ url: '/path-segment/%F0%9F%9A%80to%2Fthe%2Fmoon',
},
- ])('should resolve $input to $output', async ({ input, output }) => {
+ ])('should resolve $input to $output', async ({ input, path, url }) => {
const { router } = createTestRouter({
history: createMemoryHistory({ initialEntries: [input] }),
})
@@ -779,7 +803,8 @@ describe('encoding: URL path segment', () => {
render(() => )
await router.load()
- expect(router.state.location.pathname).toBe(output)
+ expect(router.state.location.pathname).toBe(path)
+ expect(new URL(router.state.location.url).pathname).toBe(url)
})
})
diff --git a/packages/solid-router/tests/useNavigate.test.tsx b/packages/solid-router/tests/useNavigate.test.tsx
index 6acbf30f2b0..c660f6d64f8 100644
--- a/packages/solid-router/tests/useNavigate.test.tsx
+++ b/packages/solid-router/tests/useNavigate.test.tsx
@@ -2641,3 +2641,119 @@ describe('splat routes with empty splat', () => {
},
)
})
+
+describe('encoded and unicode paths', () => {
+ const testCases = [
+ {
+ name: 'with prefix',
+ path: '/foo/prefix@대{$}',
+ expectedPath:
+ '/foo/prefix@%EB%8C%80test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]',
+ expectedLocation: '/foo/prefix@대test[s%5C/.%5C/parameter%25!🚀@]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with suffix',
+ path: '/foo/{$}대suffix@',
+ expectedPath:
+ '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80@]%EB%8C%80suffix@',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀@]대suffix@',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀@]',
+ '*': 'test[s\\/.\\/parameter%!🚀@]',
+ },
+ },
+ {
+ name: 'with wildcard',
+ path: '/foo/$',
+ expectedPath: '/foo/test[s%5C/.%5C/parameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C/.%5C/parameter%25!🚀]',
+ params: {
+ _splat: 'test[s\\/.\\/parameter%!🚀]',
+ '*': 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ // '/' is left as is with splat params but encoded with normal params
+ {
+ name: 'with path param',
+ path: `/foo/$id`,
+ expectedPath: '/foo/test[s%5C%2F.%5C%2Fparameter%25!%F0%9F%9A%80]',
+ expectedLocation: '/foo/test[s%5C%2F.%5C%2Fparameter%25!🚀]',
+ params: {
+ id: 'test[s\\/.\\/parameter%!🚀]',
+ },
+ },
+ ]
+
+ test.each(testCases)(
+ 'should handle encoded, decoded paths with unicode characters correctly - $name',
+ async ({ path, expectedPath, expectedLocation, params }) => {
+ const rootRoute = createRootRoute()
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: IndexComponent,
+ })
+
+ function IndexComponent() {
+ const navigate = useNavigate()
+
+ return (
+ <>
+ Index Route
+
+ >
+ )
+ }
+
+ const pathRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path,
+ component: PathRouteComponent,
+ })
+
+ function PathRouteComponent() {
+ const params = pathRoute.useParams()
+ return (
+
+
Path Route
+
+ params:{' '}
+
+ {JSON.stringify(params())}
+
+
+
+ )
+ }
+
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([indexRoute, pathRoute]),
+ history,
+ })
+
+ render(() => )
+
+ const link = await screen.findByTestId('btn-to-path')
+
+ fireEvent.click(link)
+
+ await waitFor(async () => {
+ const paramsToValidate = await screen.findByTestId('params-to-validate')
+
+ expect(window.location.pathname).toBe(expectedPath)
+ expect(router.latestLocation.pathname).toBe(expectedLocation)
+ expect(paramsToValidate.textContent).toEqual(JSON.stringify(params))
+ })
+ },
+ )
+})
diff --git a/packages/start-plugin-core/src/prerender.ts b/packages/start-plugin-core/src/prerender.ts
index 8c22633b374..97c7b2a5dd3 100644
--- a/packages/start-plugin-core/src/prerender.ts
+++ b/packages/start-plugin-core/src/prerender.ts
@@ -178,10 +178,9 @@ export async function prerender({
const retries = retriesByPath.get(page.path) || 0
try {
// Fetch the route
- const encodedRoute = encodeURI(page.path)
const res = await localFetch(
- withBase(encodedRoute, routerBasePath),
+ withBase(page.path, routerBasePath),
{
headers: {
...(prerenderOptions.headers ?? {}),