diff --git a/e2e/react-start/basic/package.json b/e2e/react-start/basic/package.json
index a32278fd863..839e4d55564 100644
--- a/e2e/react-start/basic/package.json
+++ b/e2e/react-start/basic/package.json
@@ -10,14 +10,13 @@
"build:spa": "MODE=spa vite build && tsc --noEmit",
"build:prerender": "MODE=prerender vite build && tsc --noEmit",
"preview": "vite preview",
- "start": "pnpx srvx --prod -s ../client dist/server/server.js",
- "start:spa": "node server.js",
+ "start": "node server.js",
"test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' &",
"test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'",
- "test:e2e:spaMode": "rm -rf port*.txt; MODE=spa playwright test --project=chromium",
- "test:e2e:ssrMode": "rm -rf port*.txt; playwright test --project=chromium",
- "test:e2e:prerender": "rm -rf port*.txt; MODE=prerender playwright test --project=chromium",
- "test:e2e:preview": "rm -rf port*.txt; MODE=preview playwright test --project=chromium",
+ "test:e2e:spaMode": "rm -rf dist; rm -rf port*.txt; MODE=spa playwright test --project=chromium",
+ "test:e2e:ssrMode": "rm -rf dist; rm -rf port*.txt; playwright test --project=chromium",
+ "test:e2e:prerender": "rm -rf dist; rm -rf port*.txt; MODE=prerender playwright test --project=chromium",
+ "test:e2e:preview": "rm -rf dist; rm -rf port*.txt; MODE=preview playwright test --project=chromium",
"test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode && pnpm run test:e2e:prerender && pnpm run test:e2e:preview"
},
"dependencies": {
diff --git a/e2e/react-start/basic/playwright.config.ts b/e2e/react-start/basic/playwright.config.ts
index aa29067f463..86c58bc1ce3 100644
--- a/e2e/react-start/basic/playwright.config.ts
+++ b/e2e/react-start/basic/playwright.config.ts
@@ -16,7 +16,7 @@ const START_PORT = await getTestServerPort(
)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
-const spaModeCommand = `pnpm build:spa && pnpm start:spa`
+const spaModeCommand = `pnpm build:spa && pnpm start`
const ssrModeCommand = `pnpm build && pnpm start`
const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && pnpm build:prerender && pnpm run test:e2e:stopDummyServer && pnpm start`
const previewModeCommand = `pnpm build && pnpm preview --port ${PORT}`
diff --git a/e2e/react-start/basic/server.js b/e2e/react-start/basic/server.js
index d618ab4bce3..83f5ff0079c 100644
--- a/e2e/react-start/basic/server.js
+++ b/e2e/react-start/basic/server.js
@@ -7,13 +7,18 @@ const port = process.env.PORT || 3000
const startPort = process.env.START_PORT || 3001
+const isSpaMode = process.env.MODE === 'spa'
+const isPrerender = process.env.MODE === 'prerender'
+
export async function createStartServer() {
const server = (await import('./dist/server/server.js')).default
const nodeHandler = toNodeHandler(server.fetch)
const app = express()
- app.use(express.static('./dist/client'))
+ // to keep testing uniform stop express from redirecting /posts to /posts/
+ // when serving pre-rendered pages
+ app.use(express.static('./dist/client', { redirect: !isPrerender }))
app.use(async (req, res, next) => {
try {
@@ -54,14 +59,22 @@ export async function createSpaServer() {
return { app }
}
-createSpaServer().then(async ({ app }) =>
- app.listen(port, () => {
- console.info(`Client Server: http://localhost:${port}`)
- }),
-)
-
-createStartServer().then(async ({ app }) =>
- app.listen(startPort, () => {
- console.info(`Start Server: http://localhost:${startPort}`)
- }),
-)
+if (isSpaMode) {
+ createSpaServer().then(async ({ app }) =>
+ app.listen(port, () => {
+ console.info(`Client Server: http://localhost:${port}`)
+ }),
+ )
+
+ createStartServer().then(async ({ app }) =>
+ app.listen(startPort, () => {
+ console.info(`Start Server: http://localhost:${startPort}`)
+ }),
+ )
+} else {
+ createStartServer().then(async ({ app }) =>
+ app.listen(port, () => {
+ console.info(`Start Server: http://localhost:${port}`)
+ }),
+ )
+}
diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts
index 27d5e911452..d1b81b1f246 100644
--- a/e2e/react-start/basic/src/routeTree.gen.ts
+++ b/e2e/react-start/basic/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 UsersRouteImport } from './routes/users'
import { Route as TypeOnlyReexportRouteImport } from './routes/type-only-reexport'
import { Route as StreamRouteImport } from './routes/stream'
@@ -21,6 +20,7 @@ import { Route as InlineScriptsRouteImport } from './routes/inline-scripts'
import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as ClientOnlyRouteImport } from './routes/client-only'
import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as SpecialCharsRouteRouteImport } from './routes/specialChars/route'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
import { Route as NotFoundRouteRouteImport } from './routes/not-found/route'
import { Route as IndexRouteImport } from './routes/index'
@@ -32,6 +32,9 @@ import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as NotFoundIndexRouteImport } from './routes/not-found/index'
import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-cookie-redirect/index'
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
+import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
+import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
+import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
@@ -61,12 +64,6 @@ import { Route as RedirectTargetServerFnViaBeforeLoadRouteImport } from './route
import { Route as FooBarQuxHereRouteImport } from './routes/foo/$bar/$qux/_here'
import { Route as FooBarQuxHereIndexRouteImport } from './routes/foo/$bar/$qux/_here/index'
-const Char45824Char54620Char48124Char44397Route =
- Char45824Char54620Char48124Char44397RouteImport.update({
- id: '/대한민국',
- path: '/대한민국',
- getParentRoute: () => rootRouteImport,
- } as any)
const UsersRoute = UsersRouteImport.update({
id: '/users',
path: '/users',
@@ -121,6 +118,11 @@ const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
+const SpecialCharsRouteRoute = SpecialCharsRouteRouteImport.update({
+ id: '/specialChars',
+ path: '/specialChars',
+ getParentRoute: () => rootRouteImport,
+} as any)
const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
id: '/search-params',
path: '/search-params',
@@ -177,6 +179,22 @@ const UsersUserIdRoute = UsersUserIdRouteImport.update({
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
+const SpecialCharsChar45824Char54620Char48124Char44397Route =
+ SpecialCharsChar45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => SpecialCharsRouteRoute,
+ } as any)
+const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
+const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
+ id: '/$param',
+ path: '/$param',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
const SearchParamsLoaderThrowsRedirectRoute =
SearchParamsLoaderThrowsRedirectRouteImport.update({
id: '/loader-throws-redirect',
@@ -328,6 +346,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/client-only': typeof ClientOnlyRoute
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
@@ -338,7 +357,6 @@ export interface FileRoutesByFullPath {
'/stream': typeof StreamRoute
'/type-only-reexport': typeof TypeOnlyReexportRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -353,6 +371,9 @@ export interface FileRoutesByFullPath {
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
@@ -377,6 +398,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/client-only': typeof ClientOnlyRoute
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
@@ -384,7 +406,6 @@ export interface FileRoutesByTo {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/type-only-reexport': typeof TypeOnlyReexportRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -398,6 +419,9 @@ export interface FileRoutesByTo {
'/raw-stream/ssr-text-hint': typeof RawStreamSsrTextHintRoute
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
'/not-found': typeof NotFoundIndexRoute
@@ -424,6 +448,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/client-only': typeof ClientOnlyRoute
'/deferred': typeof DeferredRoute
@@ -435,7 +460,6 @@ export interface FileRoutesById {
'/stream': typeof StreamRoute
'/type-only-reexport': typeof TypeOnlyReexportRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
@@ -451,6 +475,9 @@ export interface FileRoutesById {
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect/': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
@@ -479,6 +506,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/client-only'
| '/deferred'
| '/inline-scripts'
@@ -489,7 +517,6 @@ export interface FileRouteTypes {
| '/stream'
| '/type-only-reexport'
| '/users'
- | '/대한민국'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -504,6 +531,9 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect'
| '/not-found/'
@@ -528,6 +558,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/specialChars'
| '/client-only'
| '/deferred'
| '/inline-scripts'
@@ -535,7 +566,6 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/type-only-reexport'
- | '/대한민국'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -549,6 +579,9 @@ export interface FileRouteTypes {
| '/raw-stream/ssr-text-hint'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect'
| '/not-found'
@@ -574,6 +607,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/_layout'
| '/client-only'
| '/deferred'
@@ -585,7 +619,6 @@ export interface FileRouteTypes {
| '/stream'
| '/type-only-reexport'
| '/users'
- | '/대한민국'
| '/_layout/_layout-2'
| '/api/users'
| '/multi-cookie-redirect/target'
@@ -601,6 +634,9 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect/'
| '/not-found/'
@@ -628,6 +664,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
NotFoundRouteRoute: typeof NotFoundRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
+ SpecialCharsRouteRoute: typeof SpecialCharsRouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
ClientOnlyRoute: typeof ClientOnlyRoute
DeferredRoute: typeof DeferredRoute
@@ -639,7 +676,6 @@ export interface RootRouteChildren {
StreamRoute: typeof StreamRoute
TypeOnlyReexportRoute: typeof TypeOnlyReexportRoute
UsersRoute: typeof UsersRouteWithChildren
- Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
ApiUsersRoute: typeof ApiUsersRouteWithChildren
MultiCookieRedirectTargetRoute: typeof MultiCookieRedirectTargetRoute
RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
@@ -651,13 +687,6 @@ export interface RootRouteChildren {
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
- '/대한민국': {
- id: '/대한민국'
- path: '/대한민국'
- fullPath: '/대한민국'
- preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
- parentRoute: typeof rootRouteImport
- }
'/users': {
id: '/users'
path: '/users'
@@ -735,6 +764,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
+ '/specialChars': {
+ id: '/specialChars'
+ path: '/specialChars'
+ fullPath: '/specialChars'
+ preLoaderRoute: typeof SpecialCharsRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/search-params': {
id: '/search-params'
path: '/search-params'
@@ -812,6 +848,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof UsersUserIdRouteImport
parentRoute: typeof UsersRoute
}
+ '/specialChars/대한민국': {
+ id: '/specialChars/대한민국'
+ path: '/대한민국'
+ fullPath: '/specialChars/대한민국'
+ preLoaderRoute: typeof SpecialCharsChar45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
+ '/specialChars/search': {
+ id: '/specialChars/search'
+ path: '/search'
+ fullPath: '/specialChars/search'
+ preLoaderRoute: typeof SpecialCharsSearchRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
+ '/specialChars/$param': {
+ id: '/specialChars/$param'
+ path: '/$param'
+ fullPath: '/specialChars/$param'
+ preLoaderRoute: typeof SpecialCharsParamRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/search-params/loader-throws-redirect': {
id: '/search-params/loader-throws-redirect'
path: '/loader-throws-redirect'
@@ -1042,6 +1099,22 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface SpecialCharsRouteRouteChildren {
+ SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
+ SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
+}
+
+const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
+ SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsSearchRoute: SpecialCharsSearchRoute,
+ SpecialCharsChar45824Char54620Char48124Char44397Route:
+ SpecialCharsChar45824Char54620Char48124Char44397Route,
+}
+
+const SpecialCharsRouteRouteWithChildren =
+ SpecialCharsRouteRoute._addFileChildren(SpecialCharsRouteRouteChildren)
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -1169,6 +1242,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ SpecialCharsRouteRoute: SpecialCharsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
ClientOnlyRoute: ClientOnlyRoute,
DeferredRoute: DeferredRoute,
@@ -1180,8 +1254,6 @@ const rootRouteChildren: RootRouteChildren = {
StreamRoute: StreamRoute,
TypeOnlyReexportRoute: TypeOnlyReexportRoute,
UsersRoute: UsersRouteWithChildren,
- Char45824Char54620Char48124Char44397Route:
- Char45824Char54620Char48124Char44397Route,
ApiUsersRoute: ApiUsersRouteWithChildren,
MultiCookieRedirectTargetRoute: MultiCookieRedirectTargetRoute,
RedirectTargetRoute: RedirectTargetRouteWithChildren,
diff --git a/e2e/react-start/basic/src/routes/specialChars/$param.tsx b/e2e/react-start/basic/src/routes/specialChars/$param.tsx
new file mode 100644
index 00000000000..43e742d5127
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/$param.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/specialChars/$param')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const { param } = Route.useParams()
+ return (
+
+ Hello "/specialChars/$param":{' '}
+ {param}
+
+ )
+}
diff --git a/e2e/react-start/basic/src/routes/specialChars/route.tsx b/e2e/react-start/basic/src/routes/specialChars/route.tsx
new file mode 100644
index 00000000000..9811378459e
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/route.tsx
@@ -0,0 +1,44 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/specialChars')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Hello "/specialChars"!
+
+ Unicode
+ {' '}
+
+ Unicode param
+ {' '}
+
+ Unicode search param
+ {' '}
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic/src/routes/specialChars/search.tsx b/e2e/react-start/basic/src/routes/specialChars/search.tsx
new file mode 100644
index 00000000000..152f39f527b
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/search.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from '@tanstack/react-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/specialChars/search')({
+ validateSearch: z.object({
+ searchParam: z.string(),
+ }),
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+
+ return (
+
+ Hello "/specialChars/search"!
+ {search.searchParam}
+
+ )
+}
diff --git "a/e2e/react-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/react-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
new file mode 100644
index 00000000000..555a8518908
--- /dev/null
+++ "b/e2e/react-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/specialChars/대한민국')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/specialChars/대한민국"!
+
+ )
+}
diff --git "a/e2e/react-start/basic/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/react-start/basic/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-start/basic/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-start/basic/tests/params.spec.ts b/e2e/react-start/basic/tests/params.spec.ts
deleted file mode 100644
index 505e63ef433..00000000000
--- a/e2e/react-start/basic/tests/params.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { expect } from '@playwright/test'
-
-import { test } from '@tanstack/router-e2e-utils'
-
-test.beforeEach(async ({ page }) => {
- await page.goto('/')
-})
-
-test.use({
- whitelistErrors: [
- 'Failed to load resource: the server responded with a status of 404',
- ],
-})
-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-start/basic/tests/prerendering.spec.ts b/e2e/react-start/basic/tests/prerendering.spec.ts
index 7718fe86ef0..8506ff9b061 100644
--- a/e2e/react-start/basic/tests/prerendering.spec.ts
+++ b/e2e/react-start/basic/tests/prerendering.spec.ts
@@ -17,7 +17,9 @@ test.describe('Prerender Static Path Discovery', () => {
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
- expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)
+ expect(
+ existsSync(join(distDir, 'specialChars/대한민국/index.html')),
+ ).toBe(true)
// Pathless layouts should NOT be prerendered (they start with _)
expect(existsSync(join(distDir, '_layout', 'index.html'))).toBe(false) // /_layout
diff --git a/e2e/react-start/basic/tests/special-characters.spec.ts b/e2e/react-start/basic/tests/special-characters.spec.ts
new file mode 100644
index 00000000000..b583e28fdd8
--- /dev/null
+++ b/e2e/react-start/basic/tests/special-characters.spec.ts
@@ -0,0 +1,104 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.use({
+ whitelistErrors: [
+ /Failed to load resource: the server responded with a status of 404/,
+ ],
+})
+test.describe('Unicode route rendering', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/specialChars')
+ })
+
+ test('should render non-latin route correctly with direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/대한민국')
+ await page.waitForURL(
+ `${baseURL}/specialChars/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`,
+ )
+
+ await expect(page.getByTestId('special-non-latin-heading')).toBeInViewport()
+ })
+
+ test('should render non-latin route correctly during router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const nonLatinLink = page.getByTestId('special-non-latin-link')
+
+ await nonLatinLink.click()
+ await page.waitForURL(
+ `${baseURL}/specialChars/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`,
+ )
+
+ await expect(page.getByTestId('special-non-latin-heading')).toBeInViewport()
+ })
+
+ test.describe('Special characters in path params', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/대|')
+ await page.waitForURL(`${baseURL}/specialChars/%EB%8C%80%7C`)
+
+ const param = await page.getByTestId('special-param').textContent()
+
+ expect(param).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const link = page.getByTestId('special-param-link')
+
+ await link.click()
+ await page.waitForURL(`${baseURL}/specialChars/%EB%8C%80%7C`)
+
+ const param = await page.getByTestId('special-param').textContent()
+
+ expect(param).toBe('대|')
+ })
+ })
+
+ test.describe('Special characters in search params', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/search?searchParam=대|')
+
+ await page.waitForURL(
+ `${baseURL}/specialChars/search?searchParam=%EB%8C%80|`,
+ )
+
+ const searchParam = await page
+ .getByTestId('special-search-param')
+ .textContent()
+
+ expect(searchParam).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const link = page.getByTestId('special-searchParam-link')
+
+ await link.click()
+ await page.waitForURL(
+ `${baseURL}/specialChars/search?searchParam=%EB%8C%80%7C`,
+ )
+
+ const searchParam = await page
+ .getByTestId('special-search-param')
+ .textContent()
+
+ expect(searchParam).toBe('대|')
+ })
+ })
+})
diff --git a/e2e/react-start/basic/vite.config.ts b/e2e/react-start/basic/vite.config.ts
index 55c716bdb82..88f1d7690f6 100644
--- a/e2e/react-start/basic/vite.config.ts
+++ b/e2e/react-start/basic/vite.config.ts
@@ -22,6 +22,7 @@ const prerenderConfiguration = {
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
+ '/specialChars/search',
'/users',
].some((p) => page.path.includes(p)),
maxRedirects: 100,
diff --git a/e2e/react-start/virtual-routes/routes.ts b/e2e/react-start/virtual-routes/routes.ts
index ab17b8f58b4..37573a05671 100644
--- a/e2e/react-start/virtual-routes/routes.ts
+++ b/e2e/react-start/virtual-routes/routes.ts
@@ -21,4 +21,5 @@ export const routes = rootRoute('root.tsx', [
]),
]),
physical('/classic', 'file-based-subtree'),
+ route('/special|pipe', 'pipe.tsx'),
])
diff --git a/e2e/react-start/virtual-routes/src/routeTree.gen.ts b/e2e/react-start/virtual-routes/src/routeTree.gen.ts
index 540b02d9481..642eb62495e 100644
--- a/e2e/react-start/virtual-routes/src/routeTree.gen.ts
+++ b/e2e/react-start/virtual-routes/src/routeTree.gen.ts
@@ -9,6 +9,7 @@
// 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 pipeRouteImport } from './routes/pipe'
import { Route as postsPostsRouteImport } from './routes/posts/posts'
import { Route as layoutFirstLayoutRouteImport } from './routes/layout/first-layout'
import { Route as homeRouteImport } from './routes/home'
@@ -22,6 +23,11 @@ import { Route as ClassicHelloUniverseRouteImport } from './routes/file-based-su
import { Route as bRouteImport } from './routes/b'
import { Route as aRouteImport } from './routes/a'
+const pipeRoute = pipeRouteImport.update({
+ id: '/special|pipe',
+ path: '/special|pipe',
+ getParentRoute: () => rootRouteImport,
+} as any)
const postsPostsRoute = postsPostsRouteImport.update({
id: '/posts',
path: '/posts',
@@ -84,6 +90,7 @@ const aRoute = aRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof homeRoute
'/posts': typeof postsPostsRouteWithChildren
+ '/special|pipe': typeof pipeRoute
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
'/posts/': typeof postsPostsHomeRoute
'/posts/$postId': typeof postsPostsDetailRoute
@@ -95,6 +102,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof homeRoute
+ '/special|pipe': typeof pipeRoute
'/posts': typeof postsPostsHomeRoute
'/posts/$postId': typeof postsPostsDetailRoute
'/classic/hello/universe': typeof ClassicHelloUniverseRoute
@@ -108,6 +116,7 @@ export interface FileRoutesById {
'/': typeof homeRoute
'/_first': typeof layoutFirstLayoutRouteWithChildren
'/posts': typeof postsPostsRouteWithChildren
+ '/special|pipe': typeof pipeRoute
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
'/posts/': typeof postsPostsHomeRoute
'/_first/_second-layout': typeof layoutSecondLayoutRouteWithChildren
@@ -123,6 +132,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/posts'
+ | '/special|pipe'
| '/classic/hello'
| '/posts/'
| '/posts/$postId'
@@ -134,6 +144,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/special|pipe'
| '/posts'
| '/posts/$postId'
| '/classic/hello/universe'
@@ -146,6 +157,7 @@ export interface FileRouteTypes {
| '/'
| '/_first'
| '/posts'
+ | '/special|pipe'
| '/classic/hello'
| '/posts/'
| '/_first/_second-layout'
@@ -161,11 +173,19 @@ export interface RootRouteChildren {
homeRoute: typeof homeRoute
layoutFirstLayoutRoute: typeof layoutFirstLayoutRouteWithChildren
postsPostsRoute: typeof postsPostsRouteWithChildren
+ pipeRoute: typeof pipeRoute
ClassicHelloRouteRoute: typeof ClassicHelloRouteRouteWithChildren
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
+ '/special|pipe': {
+ id: '/special|pipe'
+ path: '/special|pipe'
+ fullPath: '/special|pipe'
+ preLoaderRoute: typeof pipeRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/posts': {
id: '/posts'
path: '/posts'
@@ -310,6 +330,7 @@ const rootRouteChildren: RootRouteChildren = {
homeRoute: homeRoute,
layoutFirstLayoutRoute: layoutFirstLayoutRouteWithChildren,
postsPostsRoute: postsPostsRouteWithChildren,
+ pipeRoute: pipeRoute,
ClassicHelloRouteRoute: ClassicHelloRouteRouteWithChildren,
}
export const routeTree = rootRouteImport
diff --git a/e2e/react-start/virtual-routes/src/routes/pipe.tsx b/e2e/react-start/virtual-routes/src/routes/pipe.tsx
new file mode 100644
index 00000000000..e4077f0db85
--- /dev/null
+++ b/e2e/react-start/virtual-routes/src/routes/pipe.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/special|pipe')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+ Hello "/special|pipe"!
+ )
+}
diff --git a/e2e/react-start/virtual-routes/src/routes/root.tsx b/e2e/react-start/virtual-routes/src/routes/root.tsx
index 19f23011b7e..c0035a41108 100644
--- a/e2e/react-start/virtual-routes/src/routes/root.tsx
+++ b/e2e/react-start/virtual-routes/src/routes/root.tsx
@@ -76,6 +76,15 @@ function RootDocument({ children }: { children: React.ReactNode }) {
>
Subtree
{' '}
+
+ Pipe
+ {' '}
{
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+ })
+
+ test.describe('Special characters in route paths', () => {
+ test('should render route with pipe character in path on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/special|pipe')
+ await page.waitForURL(`${baseURL}/special%7Cpipe`)
+
+ await expect(
+ page.getByTestId('special-pipe-route-heading'),
+ ).toBeInViewport()
+ })
+
+ test('should render route with pipe character in path on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const pipeLink = page.getByTestId('special-pipe-link')
+
+ await pipeLink.click()
+ await page.waitForURL(`${baseURL}/special%7Cpipe`)
+
+ await expect(
+ page.getByTestId('special-pipe-route-heading'),
+ ).toBeInViewport()
+ })
+ })
+})
diff --git a/e2e/solid-start/basic/package.json b/e2e/solid-start/basic/package.json
index 483839b1238..4b9899fb607 100644
--- a/e2e/solid-start/basic/package.json
+++ b/e2e/solid-start/basic/package.json
@@ -10,8 +10,7 @@
"build:spa": "MODE=spa vite build && tsc --noEmit",
"build:prerender": "MODE=prerender vite build && tsc --noEmit",
"preview": "vite preview",
- "start": "pnpx srvx --prod -s ../client dist/server/server.js",
- "start:spa": "node server.js",
+ "start": "node server.js",
"test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' &",
"test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'",
"test:e2e:spaMode": "rm -rf port*.txt; MODE=spa playwright test --project=chromium",
diff --git a/e2e/solid-start/basic/playwright.config.ts b/e2e/solid-start/basic/playwright.config.ts
index aa29067f463..86c58bc1ce3 100644
--- a/e2e/solid-start/basic/playwright.config.ts
+++ b/e2e/solid-start/basic/playwright.config.ts
@@ -16,7 +16,7 @@ const START_PORT = await getTestServerPort(
)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
-const spaModeCommand = `pnpm build:spa && pnpm start:spa`
+const spaModeCommand = `pnpm build:spa && pnpm start`
const ssrModeCommand = `pnpm build && pnpm start`
const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && pnpm build:prerender && pnpm run test:e2e:stopDummyServer && pnpm start`
const previewModeCommand = `pnpm build && pnpm preview --port ${PORT}`
diff --git a/e2e/solid-start/basic/server.js b/e2e/solid-start/basic/server.js
index d618ab4bce3..83f5ff0079c 100644
--- a/e2e/solid-start/basic/server.js
+++ b/e2e/solid-start/basic/server.js
@@ -7,13 +7,18 @@ const port = process.env.PORT || 3000
const startPort = process.env.START_PORT || 3001
+const isSpaMode = process.env.MODE === 'spa'
+const isPrerender = process.env.MODE === 'prerender'
+
export async function createStartServer() {
const server = (await import('./dist/server/server.js')).default
const nodeHandler = toNodeHandler(server.fetch)
const app = express()
- app.use(express.static('./dist/client'))
+ // to keep testing uniform stop express from redirecting /posts to /posts/
+ // when serving pre-rendered pages
+ app.use(express.static('./dist/client', { redirect: !isPrerender }))
app.use(async (req, res, next) => {
try {
@@ -54,14 +59,22 @@ export async function createSpaServer() {
return { app }
}
-createSpaServer().then(async ({ app }) =>
- app.listen(port, () => {
- console.info(`Client Server: http://localhost:${port}`)
- }),
-)
-
-createStartServer().then(async ({ app }) =>
- app.listen(startPort, () => {
- console.info(`Start Server: http://localhost:${startPort}`)
- }),
-)
+if (isSpaMode) {
+ createSpaServer().then(async ({ app }) =>
+ app.listen(port, () => {
+ console.info(`Client Server: http://localhost:${port}`)
+ }),
+ )
+
+ createStartServer().then(async ({ app }) =>
+ app.listen(startPort, () => {
+ console.info(`Start Server: http://localhost:${startPort}`)
+ }),
+ )
+} else {
+ createStartServer().then(async ({ app }) =>
+ app.listen(port, () => {
+ console.info(`Start Server: http://localhost:${port}`)
+ }),
+ )
+}
diff --git a/e2e/solid-start/basic/src/routeTree.gen.ts b/e2e/solid-start/basic/src/routeTree.gen.ts
index 238008b2049..8785c63ae4c 100644
--- a/e2e/solid-start/basic/src/routeTree.gen.ts
+++ b/e2e/solid-start/basic/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 UsersRouteImport } from './routes/users'
import { Route as StreamRouteImport } from './routes/stream'
import { Route as ScriptsRouteImport } from './routes/scripts'
@@ -19,6 +18,7 @@ import { Route as LinksRouteImport } from './routes/links'
import { Route as InlineScriptsRouteImport } from './routes/inline-scripts'
import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as SpecialCharsRouteRouteImport } from './routes/specialChars/route'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
import { Route as NotFoundRouteRouteImport } from './routes/not-found/route'
import { Route as IndexRouteImport } from './routes/index'
@@ -30,6 +30,9 @@ import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as NotFoundIndexRouteImport } from './routes/not-found/index'
import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-cookie-redirect/index'
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
+import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
+import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
+import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
@@ -59,12 +62,6 @@ import { Route as RedirectTargetServerFnViaUseServerFnRouteImport } from './rout
import { Route as RedirectTargetServerFnViaLoaderRouteImport } from './routes/redirect/$target/serverFn/via-loader'
import { Route as RedirectTargetServerFnViaBeforeLoadRouteImport } from './routes/redirect/$target/serverFn/via-beforeLoad'
-const Char45824Char54620Char48124Char44397Route =
- Char45824Char54620Char48124Char44397RouteImport.update({
- id: '/대한민국',
- path: '/대한민국',
- getParentRoute: () => rootRouteImport,
- } as any)
const UsersRoute = UsersRouteImport.update({
id: '/users',
path: '/users',
@@ -109,6 +106,11 @@ const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
+const SpecialCharsRouteRoute = SpecialCharsRouteRouteImport.update({
+ id: '/specialChars',
+ path: '/specialChars',
+ getParentRoute: () => rootRouteImport,
+} as any)
const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
id: '/search-params',
path: '/search-params',
@@ -165,6 +167,22 @@ const UsersUserIdRoute = UsersUserIdRouteImport.update({
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
+const SpecialCharsChar45824Char54620Char48124Char44397Route =
+ SpecialCharsChar45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => SpecialCharsRouteRoute,
+ } as any)
+const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
+const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
+ id: '/$param',
+ path: '/$param',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
const SearchParamsLoaderThrowsRedirectRoute =
SearchParamsLoaderThrowsRedirectRouteImport.update({
id: '/loader-throws-redirect',
@@ -318,6 +336,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
'/links': typeof LinksRoute
@@ -326,7 +345,6 @@ export interface FileRoutesByFullPath {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -341,6 +359,9 @@ export interface FileRoutesByFullPath {
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
@@ -365,12 +386,12 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
'/links': typeof LinksRoute
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -384,6 +405,9 @@ export interface FileRoutesByTo {
'/raw-stream/ssr-text-hint': typeof RawStreamSsrTextHintRoute
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
'/not-found': typeof NotFoundIndexRoute
@@ -411,6 +435,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
@@ -420,7 +445,6 @@ export interface FileRoutesById {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
@@ -436,6 +460,9 @@ export interface FileRoutesById {
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect/': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
@@ -464,6 +491,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
@@ -472,7 +500,6 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -487,6 +514,9 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect'
| '/not-found/'
@@ -511,12 +541,12 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
| '/scripts'
| '/stream'
- | '/대한민국'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -530,6 +560,9 @@ export interface FileRouteTypes {
| '/raw-stream/ssr-text-hint'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect'
| '/not-found'
@@ -556,6 +589,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/_layout'
| '/deferred'
| '/inline-scripts'
@@ -565,7 +599,6 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
| '/_layout/_layout-2'
| '/api/users'
| '/multi-cookie-redirect/target'
@@ -581,6 +614,9 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect/'
| '/not-found/'
@@ -608,6 +644,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
NotFoundRouteRoute: typeof NotFoundRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
+ SpecialCharsRouteRoute: typeof SpecialCharsRouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
DeferredRoute: typeof DeferredRoute
InlineScriptsRoute: typeof InlineScriptsRoute
@@ -617,7 +654,6 @@ export interface RootRouteChildren {
ScriptsRoute: typeof ScriptsRoute
StreamRoute: typeof StreamRoute
UsersRoute: typeof UsersRouteWithChildren
- Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
ApiUsersRoute: typeof ApiUsersRouteWithChildren
MultiCookieRedirectTargetRoute: typeof MultiCookieRedirectTargetRoute
RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
@@ -630,13 +666,6 @@ export interface RootRouteChildren {
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
- '/대한민국': {
- id: '/대한민국'
- path: '/대한민국'
- fullPath: '/대한민국'
- preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
- parentRoute: typeof rootRouteImport
- }
'/users': {
id: '/users'
path: '/users'
@@ -700,6 +729,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
+ '/specialChars': {
+ id: '/specialChars'
+ path: '/specialChars'
+ fullPath: '/specialChars'
+ preLoaderRoute: typeof SpecialCharsRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/search-params': {
id: '/search-params'
path: '/search-params'
@@ -777,6 +813,27 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof UsersUserIdRouteImport
parentRoute: typeof UsersRoute
}
+ '/specialChars/대한민국': {
+ id: '/specialChars/대한민국'
+ path: '/대한민국'
+ fullPath: '/specialChars/대한민국'
+ preLoaderRoute: typeof SpecialCharsChar45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
+ '/specialChars/search': {
+ id: '/specialChars/search'
+ path: '/search'
+ fullPath: '/specialChars/search'
+ preLoaderRoute: typeof SpecialCharsSearchRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
+ '/specialChars/$param': {
+ id: '/specialChars/$param'
+ path: '/$param'
+ fullPath: '/specialChars/$param'
+ preLoaderRoute: typeof SpecialCharsParamRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/search-params/loader-throws-redirect': {
id: '/search-params/loader-throws-redirect'
path: '/loader-throws-redirect'
@@ -1007,6 +1064,22 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface SpecialCharsRouteRouteChildren {
+ SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
+ SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
+}
+
+const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
+ SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsSearchRoute: SpecialCharsSearchRoute,
+ SpecialCharsChar45824Char54620Char48124Char44397Route:
+ SpecialCharsChar45824Char54620Char48124Char44397Route,
+}
+
+const SpecialCharsRouteRouteWithChildren =
+ SpecialCharsRouteRoute._addFileChildren(SpecialCharsRouteRouteChildren)
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -1122,6 +1195,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ SpecialCharsRouteRoute: SpecialCharsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
DeferredRoute: DeferredRoute,
InlineScriptsRoute: InlineScriptsRoute,
@@ -1131,8 +1205,6 @@ const rootRouteChildren: RootRouteChildren = {
ScriptsRoute: ScriptsRoute,
StreamRoute: StreamRoute,
UsersRoute: UsersRouteWithChildren,
- Char45824Char54620Char48124Char44397Route:
- Char45824Char54620Char48124Char44397Route,
ApiUsersRoute: ApiUsersRouteWithChildren,
MultiCookieRedirectTargetRoute: MultiCookieRedirectTargetRoute,
RedirectTargetRoute: RedirectTargetRouteWithChildren,
diff --git a/e2e/solid-start/basic/src/routes/specialChars/$param.tsx b/e2e/solid-start/basic/src/routes/specialChars/$param.tsx
new file mode 100644
index 00000000000..179965e2c0c
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/$param.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/specialChars/$param')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+
+ Hello "/specialChars/$param":{' '}
+ {params().param}
+
+ )
+}
diff --git a/e2e/solid-start/basic/src/routes/specialChars/route.tsx b/e2e/solid-start/basic/src/routes/specialChars/route.tsx
new file mode 100644
index 00000000000..e57876041bb
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/route.tsx
@@ -0,0 +1,44 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/specialChars')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Hello "/specialChars"!
+
+ Unicode
+ {' '}
+
+ Unicode param
+ {' '}
+
+ Unicode search param
+ {' '}
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/src/routes/specialChars/search.tsx b/e2e/solid-start/basic/src/routes/specialChars/search.tsx
new file mode 100644
index 00000000000..9ffc8f026f0
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/search.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/specialChars/search')({
+ validateSearch: z.object({
+ searchParam: z.string(),
+ }),
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+
+ return (
+
+ Hello "/specialChars/search"!
+ {search().searchParam}
+
+ )
+}
diff --git "a/e2e/solid-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/solid-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
new file mode 100644
index 00000000000..13257e1fa89
--- /dev/null
+++ "b/e2e/solid-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/specialChars/대한민국')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/specialChars/대한민국"!
+
+ )
+}
diff --git "a/e2e/solid-start/basic/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/solid-start/basic/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-start/basic/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-start/basic/tests/params.spec.ts b/e2e/solid-start/basic/tests/params.spec.ts
deleted file mode 100644
index 505e63ef433..00000000000
--- a/e2e/solid-start/basic/tests/params.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { expect } from '@playwright/test'
-
-import { test } from '@tanstack/router-e2e-utils'
-
-test.beforeEach(async ({ page }) => {
- await page.goto('/')
-})
-
-test.use({
- whitelistErrors: [
- 'Failed to load resource: the server responded with a status of 404',
- ],
-})
-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-start/basic/tests/prerendering.spec.ts b/e2e/solid-start/basic/tests/prerendering.spec.ts
index 7718fe86ef0..8506ff9b061 100644
--- a/e2e/solid-start/basic/tests/prerendering.spec.ts
+++ b/e2e/solid-start/basic/tests/prerendering.spec.ts
@@ -17,7 +17,9 @@ test.describe('Prerender Static Path Discovery', () => {
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
- expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)
+ expect(
+ existsSync(join(distDir, 'specialChars/대한민국/index.html')),
+ ).toBe(true)
// Pathless layouts should NOT be prerendered (they start with _)
expect(existsSync(join(distDir, '_layout', 'index.html'))).toBe(false) // /_layout
diff --git a/e2e/solid-start/basic/tests/special-characters.spec.ts b/e2e/solid-start/basic/tests/special-characters.spec.ts
new file mode 100644
index 00000000000..b583e28fdd8
--- /dev/null
+++ b/e2e/solid-start/basic/tests/special-characters.spec.ts
@@ -0,0 +1,104 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.use({
+ whitelistErrors: [
+ /Failed to load resource: the server responded with a status of 404/,
+ ],
+})
+test.describe('Unicode route rendering', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/specialChars')
+ })
+
+ test('should render non-latin route correctly with direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/대한민국')
+ await page.waitForURL(
+ `${baseURL}/specialChars/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`,
+ )
+
+ await expect(page.getByTestId('special-non-latin-heading')).toBeInViewport()
+ })
+
+ test('should render non-latin route correctly during router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const nonLatinLink = page.getByTestId('special-non-latin-link')
+
+ await nonLatinLink.click()
+ await page.waitForURL(
+ `${baseURL}/specialChars/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`,
+ )
+
+ await expect(page.getByTestId('special-non-latin-heading')).toBeInViewport()
+ })
+
+ test.describe('Special characters in path params', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/대|')
+ await page.waitForURL(`${baseURL}/specialChars/%EB%8C%80%7C`)
+
+ const param = await page.getByTestId('special-param').textContent()
+
+ expect(param).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const link = page.getByTestId('special-param-link')
+
+ await link.click()
+ await page.waitForURL(`${baseURL}/specialChars/%EB%8C%80%7C`)
+
+ const param = await page.getByTestId('special-param').textContent()
+
+ expect(param).toBe('대|')
+ })
+ })
+
+ test.describe('Special characters in search params', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/search?searchParam=대|')
+
+ await page.waitForURL(
+ `${baseURL}/specialChars/search?searchParam=%EB%8C%80|`,
+ )
+
+ const searchParam = await page
+ .getByTestId('special-search-param')
+ .textContent()
+
+ expect(searchParam).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const link = page.getByTestId('special-searchParam-link')
+
+ await link.click()
+ await page.waitForURL(
+ `${baseURL}/specialChars/search?searchParam=%EB%8C%80%7C`,
+ )
+
+ const searchParam = await page
+ .getByTestId('special-search-param')
+ .textContent()
+
+ expect(searchParam).toBe('대|')
+ })
+ })
+})
diff --git a/e2e/solid-start/basic/vite.config.ts b/e2e/solid-start/basic/vite.config.ts
index 37a52a0ea3c..3d310300aef 100644
--- a/e2e/solid-start/basic/vite.config.ts
+++ b/e2e/solid-start/basic/vite.config.ts
@@ -22,6 +22,7 @@ const prerenderConfiguration = {
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
+ '/specialChars/search',
'/search-params/default',
'/transition',
'/users',
diff --git a/e2e/solid-start/virtual-routes/routes.ts b/e2e/solid-start/virtual-routes/routes.ts
index ab17b8f58b4..37573a05671 100644
--- a/e2e/solid-start/virtual-routes/routes.ts
+++ b/e2e/solid-start/virtual-routes/routes.ts
@@ -21,4 +21,5 @@ export const routes = rootRoute('root.tsx', [
]),
]),
physical('/classic', 'file-based-subtree'),
+ route('/special|pipe', 'pipe.tsx'),
])
diff --git a/e2e/solid-start/virtual-routes/src/routeTree.gen.ts b/e2e/solid-start/virtual-routes/src/routeTree.gen.ts
index 6b10324a1ff..cb662d8a5d6 100644
--- a/e2e/solid-start/virtual-routes/src/routeTree.gen.ts
+++ b/e2e/solid-start/virtual-routes/src/routeTree.gen.ts
@@ -9,6 +9,7 @@
// 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 pipeRouteImport } from './routes/pipe'
import { Route as postsPostsRouteImport } from './routes/posts/posts'
import { Route as layoutFirstLayoutRouteImport } from './routes/layout/first-layout'
import { Route as homeRouteImport } from './routes/home'
@@ -22,6 +23,11 @@ import { Route as ClassicHelloUniverseRouteImport } from './routes/file-based-su
import { Route as bRouteImport } from './routes/b'
import { Route as aRouteImport } from './routes/a'
+const pipeRoute = pipeRouteImport.update({
+ id: '/special|pipe',
+ path: '/special|pipe',
+ getParentRoute: () => rootRouteImport,
+} as any)
const postsPostsRoute = postsPostsRouteImport.update({
id: '/posts',
path: '/posts',
@@ -84,6 +90,7 @@ const aRoute = aRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof homeRoute
'/posts': typeof postsPostsRouteWithChildren
+ '/special|pipe': typeof pipeRoute
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
'/posts/': typeof postsPostsHomeRoute
'/posts/$postId': typeof postsPostsDetailRoute
@@ -95,6 +102,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof homeRoute
+ '/special|pipe': typeof pipeRoute
'/posts': typeof postsPostsHomeRoute
'/posts/$postId': typeof postsPostsDetailRoute
'/classic/hello/universe': typeof ClassicHelloUniverseRoute
@@ -108,6 +116,7 @@ export interface FileRoutesById {
'/': typeof homeRoute
'/_first': typeof layoutFirstLayoutRouteWithChildren
'/posts': typeof postsPostsRouteWithChildren
+ '/special|pipe': typeof pipeRoute
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
'/posts/': typeof postsPostsHomeRoute
'/_first/_second-layout': typeof layoutSecondLayoutRouteWithChildren
@@ -123,6 +132,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/posts'
+ | '/special|pipe'
| '/classic/hello'
| '/posts/'
| '/posts/$postId'
@@ -134,6 +144,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/special|pipe'
| '/posts'
| '/posts/$postId'
| '/classic/hello/universe'
@@ -146,6 +157,7 @@ export interface FileRouteTypes {
| '/'
| '/_first'
| '/posts'
+ | '/special|pipe'
| '/classic/hello'
| '/posts/'
| '/_first/_second-layout'
@@ -161,11 +173,19 @@ export interface RootRouteChildren {
homeRoute: typeof homeRoute
layoutFirstLayoutRoute: typeof layoutFirstLayoutRouteWithChildren
postsPostsRoute: typeof postsPostsRouteWithChildren
+ pipeRoute: typeof pipeRoute
ClassicHelloRouteRoute: typeof ClassicHelloRouteRouteWithChildren
}
declare module '@tanstack/solid-router' {
interface FileRoutesByPath {
+ '/special|pipe': {
+ id: '/special|pipe'
+ path: '/special|pipe'
+ fullPath: '/special|pipe'
+ preLoaderRoute: typeof pipeRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/posts': {
id: '/posts'
path: '/posts'
@@ -310,6 +330,7 @@ const rootRouteChildren: RootRouteChildren = {
homeRoute: homeRoute,
layoutFirstLayoutRoute: layoutFirstLayoutRouteWithChildren,
postsPostsRoute: postsPostsRouteWithChildren,
+ pipeRoute: pipeRoute,
ClassicHelloRouteRoute: ClassicHelloRouteRouteWithChildren,
}
export const routeTree = rootRouteImport
diff --git a/e2e/solid-start/virtual-routes/src/routes/pipe.tsx b/e2e/solid-start/virtual-routes/src/routes/pipe.tsx
new file mode 100644
index 00000000000..009b116c162
--- /dev/null
+++ b/e2e/solid-start/virtual-routes/src/routes/pipe.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/special|pipe')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+ Hello "/special|pipe"!
+ )
+}
diff --git a/e2e/solid-start/virtual-routes/src/routes/root.tsx b/e2e/solid-start/virtual-routes/src/routes/root.tsx
index ef7b0745f16..bd4ae2d4dd9 100644
--- a/e2e/solid-start/virtual-routes/src/routes/root.tsx
+++ b/e2e/solid-start/virtual-routes/src/routes/root.tsx
@@ -76,6 +76,15 @@ function RootDocument({ children }: { children: JSX.Element }) {
>
Subtree
{' '}
+
+ Pipe
+ {' '}
{
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+ })
+
+ test.describe('Special characters in route paths', () => {
+ test('should render route with pipe character in path on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/special|pipe')
+ await page.waitForURL(`${baseURL}/special%7Cpipe`)
+
+ await expect(
+ page.getByTestId('special-pipe-route-heading'),
+ ).toBeInViewport()
+ })
+
+ test('should render route with pipe character in path on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const pipeLink = page.getByTestId('special-pipe-link')
+
+ await pipeLink.click()
+ await page.waitForURL(`${baseURL}/special%7Cpipe`)
+
+ await expect(
+ page.getByTestId('special-pipe-route-heading'),
+ ).toBeInViewport()
+ })
+ })
+})
diff --git a/e2e/vue-start/basic/package.json b/e2e/vue-start/basic/package.json
index ace768f1e40..fb27b73fca2 100644
--- a/e2e/vue-start/basic/package.json
+++ b/e2e/vue-start/basic/package.json
@@ -10,8 +10,7 @@
"build:spa": "MODE=spa vite build && tsc --noEmit",
"build:prerender": "MODE=prerender vite build && tsc --noEmit",
"preview": "vite preview",
- "start": "pnpx srvx --prod -s ../client dist/server/server.js",
- "start:spa": "node server.js",
+ "start": "node server.js",
"test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' &",
"test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'",
"test:e2e:spaMode": "rm -rf port*.txt; MODE=spa playwright test --project=chromium",
diff --git a/e2e/vue-start/basic/playwright.config.ts b/e2e/vue-start/basic/playwright.config.ts
index aa29067f463..86c58bc1ce3 100644
--- a/e2e/vue-start/basic/playwright.config.ts
+++ b/e2e/vue-start/basic/playwright.config.ts
@@ -16,7 +16,7 @@ const START_PORT = await getTestServerPort(
)
const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
-const spaModeCommand = `pnpm build:spa && pnpm start:spa`
+const spaModeCommand = `pnpm build:spa && pnpm start`
const ssrModeCommand = `pnpm build && pnpm start`
const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && pnpm build:prerender && pnpm run test:e2e:stopDummyServer && pnpm start`
const previewModeCommand = `pnpm build && pnpm preview --port ${PORT}`
diff --git a/e2e/vue-start/basic/server.js b/e2e/vue-start/basic/server.js
index d618ab4bce3..83f5ff0079c 100644
--- a/e2e/vue-start/basic/server.js
+++ b/e2e/vue-start/basic/server.js
@@ -7,13 +7,18 @@ const port = process.env.PORT || 3000
const startPort = process.env.START_PORT || 3001
+const isSpaMode = process.env.MODE === 'spa'
+const isPrerender = process.env.MODE === 'prerender'
+
export async function createStartServer() {
const server = (await import('./dist/server/server.js')).default
const nodeHandler = toNodeHandler(server.fetch)
const app = express()
- app.use(express.static('./dist/client'))
+ // to keep testing uniform stop express from redirecting /posts to /posts/
+ // when serving pre-rendered pages
+ app.use(express.static('./dist/client', { redirect: !isPrerender }))
app.use(async (req, res, next) => {
try {
@@ -54,14 +59,22 @@ export async function createSpaServer() {
return { app }
}
-createSpaServer().then(async ({ app }) =>
- app.listen(port, () => {
- console.info(`Client Server: http://localhost:${port}`)
- }),
-)
-
-createStartServer().then(async ({ app }) =>
- app.listen(startPort, () => {
- console.info(`Start Server: http://localhost:${startPort}`)
- }),
-)
+if (isSpaMode) {
+ createSpaServer().then(async ({ app }) =>
+ app.listen(port, () => {
+ console.info(`Client Server: http://localhost:${port}`)
+ }),
+ )
+
+ createStartServer().then(async ({ app }) =>
+ app.listen(startPort, () => {
+ console.info(`Start Server: http://localhost:${startPort}`)
+ }),
+ )
+} else {
+ createStartServer().then(async ({ app }) =>
+ app.listen(port, () => {
+ console.info(`Start Server: http://localhost:${port}`)
+ }),
+ )
+}
diff --git a/e2e/vue-start/basic/src/routeTree.gen.ts b/e2e/vue-start/basic/src/routeTree.gen.ts
index 422ba41bb43..b8a1f2b5493 100644
--- a/e2e/vue-start/basic/src/routeTree.gen.ts
+++ b/e2e/vue-start/basic/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 UsersRouteImport } from './routes/users'
import { Route as StreamRouteImport } from './routes/stream'
import { Route as ScriptsRouteImport } from './routes/scripts'
@@ -19,6 +18,7 @@ import { Route as LinksRouteImport } from './routes/links'
import { Route as InlineScriptsRouteImport } from './routes/inline-scripts'
import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as LayoutRouteImport } from './routes/_layout'
+import { Route as SpecialCharsRouteRouteImport } from './routes/specialChars/route'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
import { Route as NotFoundRouteRouteImport } from './routes/not-found/route'
import { Route as IndexRouteImport } from './routes/index'
@@ -30,6 +30,9 @@ import { Route as PostsIndexRouteImport } from './routes/posts.index'
import { Route as NotFoundIndexRouteImport } from './routes/not-found/index'
import { Route as MultiCookieRedirectIndexRouteImport } from './routes/multi-cookie-redirect/index'
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
+import { Route as SpecialCharsChar45824Char54620Char48124Char44397RouteImport } from './routes/specialChars/대한민국'
+import { Route as SpecialCharsSearchRouteImport } from './routes/specialChars/search'
+import { Route as SpecialCharsParamRouteImport } from './routes/specialChars/$param'
import { Route as SearchParamsLoaderThrowsRedirectRouteImport } from './routes/search-params/loader-throws-redirect'
import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/default'
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
@@ -57,12 +60,6 @@ import { Route as RedirectTargetServerFnViaUseServerFnRouteImport } from './rout
import { Route as RedirectTargetServerFnViaLoaderRouteImport } from './routes/redirect/$target/serverFn/via-loader'
import { Route as RedirectTargetServerFnViaBeforeLoadRouteImport } from './routes/redirect/$target/serverFn/via-beforeLoad'
-const Char45824Char54620Char48124Char44397Route =
- Char45824Char54620Char48124Char44397RouteImport.update({
- id: '/대한민국',
- path: '/대한민국',
- getParentRoute: () => rootRouteImport,
- } as any)
const UsersRoute = UsersRouteImport.update({
id: '/users',
path: '/users',
@@ -107,6 +104,11 @@ const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
} as any)
+const SpecialCharsRouteRoute = SpecialCharsRouteRouteImport.update({
+ id: '/specialChars',
+ path: '/specialChars',
+ getParentRoute: () => rootRouteImport,
+} as any)
const SearchParamsRouteRoute = SearchParamsRouteRouteImport.update({
id: '/search-params',
path: '/search-params',
@@ -163,6 +165,22 @@ const UsersUserIdRoute = UsersUserIdRouteImport.update({
path: '/$userId',
getParentRoute: () => UsersRoute,
} as any)
+const SpecialCharsChar45824Char54620Char48124Char44397Route =
+ SpecialCharsChar45824Char54620Char48124Char44397RouteImport.update({
+ id: '/대한민국',
+ path: '/대한민국',
+ getParentRoute: () => SpecialCharsRouteRoute,
+ } as any)
+const SpecialCharsSearchRoute = SpecialCharsSearchRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
+const SpecialCharsParamRoute = SpecialCharsParamRouteImport.update({
+ id: '/$param',
+ path: '/$param',
+ getParentRoute: () => SpecialCharsRouteRoute,
+} as any)
const SearchParamsLoaderThrowsRedirectRoute =
SearchParamsLoaderThrowsRedirectRouteImport.update({
id: '/loader-throws-redirect',
@@ -304,6 +322,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
'/links': typeof LinksRoute
@@ -312,7 +331,6 @@ export interface FileRoutesByFullPath {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -327,6 +345,9 @@ export interface FileRoutesByFullPath {
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
@@ -349,12 +370,12 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
'/links': typeof LinksRoute
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -368,6 +389,9 @@ export interface FileRoutesByTo {
'/raw-stream/ssr-text-hint': typeof RawStreamSsrTextHintRoute
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect': typeof MultiCookieRedirectIndexRoute
'/not-found': typeof NotFoundIndexRoute
@@ -393,6 +417,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
+ '/specialChars': typeof SpecialCharsRouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
@@ -402,7 +427,6 @@ export interface FileRoutesById {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
@@ -418,6 +442,9 @@ export interface FileRoutesById {
'/redirect/$target': typeof RedirectTargetRouteWithChildren
'/search-params/default': typeof SearchParamsDefaultRoute
'/search-params/loader-throws-redirect': typeof SearchParamsLoaderThrowsRedirectRoute
+ '/specialChars/$param': typeof SpecialCharsParamRoute
+ '/specialChars/search': typeof SpecialCharsSearchRoute
+ '/specialChars/대한민국': typeof SpecialCharsChar45824Char54620Char48124Char44397Route
'/users/$userId': typeof UsersUserIdRoute
'/multi-cookie-redirect/': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
@@ -444,6 +471,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
@@ -452,7 +480,6 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -467,6 +494,9 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect'
| '/not-found/'
@@ -489,12 +519,12 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
| '/scripts'
| '/stream'
- | '/대한민국'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -508,6 +538,9 @@ export interface FileRouteTypes {
| '/raw-stream/ssr-text-hint'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect'
| '/not-found'
@@ -532,6 +565,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/_layout'
| '/deferred'
| '/inline-scripts'
@@ -541,7 +575,6 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
| '/_layout/_layout-2'
| '/api/users'
| '/multi-cookie-redirect/target'
@@ -557,6 +590,9 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
| '/multi-cookie-redirect/'
| '/not-found/'
@@ -582,6 +618,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
NotFoundRouteRoute: typeof NotFoundRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
+ SpecialCharsRouteRoute: typeof SpecialCharsRouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
DeferredRoute: typeof DeferredRoute
InlineScriptsRoute: typeof InlineScriptsRoute
@@ -591,7 +628,6 @@ export interface RootRouteChildren {
ScriptsRoute: typeof ScriptsRoute
StreamRoute: typeof StreamRoute
UsersRoute: typeof UsersRouteWithChildren
- Char45824Char54620Char48124Char44397Route: typeof Char45824Char54620Char48124Char44397Route
ApiUsersRoute: typeof ApiUsersRouteWithChildren
MultiCookieRedirectTargetRoute: typeof MultiCookieRedirectTargetRoute
RedirectTargetRoute: typeof RedirectTargetRouteWithChildren
@@ -602,13 +638,6 @@ export interface RootRouteChildren {
declare module '@tanstack/vue-router' {
interface FileRoutesByPath {
- '/대한민국': {
- id: '/대한민국'
- path: '/대한민국'
- fullPath: '/대한민국'
- preLoaderRoute: typeof Char45824Char54620Char48124Char44397RouteImport
- parentRoute: typeof rootRouteImport
- }
'/users': {
id: '/users'
path: '/users'
@@ -672,6 +701,13 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof LayoutRouteImport
parentRoute: typeof rootRouteImport
}
+ '/specialChars': {
+ id: '/specialChars'
+ path: '/specialChars'
+ fullPath: '/specialChars'
+ preLoaderRoute: typeof SpecialCharsRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/search-params': {
id: '/search-params'
path: '/search-params'
@@ -749,6 +785,27 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof UsersUserIdRouteImport
parentRoute: typeof UsersRoute
}
+ '/specialChars/대한민국': {
+ id: '/specialChars/대한민국'
+ path: '/대한민국'
+ fullPath: '/specialChars/대한민국'
+ preLoaderRoute: typeof SpecialCharsChar45824Char54620Char48124Char44397RouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
+ '/specialChars/search': {
+ id: '/specialChars/search'
+ path: '/search'
+ fullPath: '/specialChars/search'
+ preLoaderRoute: typeof SpecialCharsSearchRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
+ '/specialChars/$param': {
+ id: '/specialChars/$param'
+ path: '/$param'
+ fullPath: '/specialChars/$param'
+ preLoaderRoute: typeof SpecialCharsParamRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/search-params/loader-throws-redirect': {
id: '/search-params/loader-throws-redirect'
path: '/loader-throws-redirect'
@@ -965,6 +1022,22 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface SpecialCharsRouteRouteChildren {
+ SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
+ SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
+}
+
+const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
+ SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsSearchRoute: SpecialCharsSearchRoute,
+ SpecialCharsChar45824Char54620Char48124Char44397Route:
+ SpecialCharsChar45824Char54620Char48124Char44397Route,
+}
+
+const SpecialCharsRouteRouteWithChildren =
+ SpecialCharsRouteRoute._addFileChildren(SpecialCharsRouteRouteChildren)
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -1080,6 +1153,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ SpecialCharsRouteRoute: SpecialCharsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
DeferredRoute: DeferredRoute,
InlineScriptsRoute: InlineScriptsRoute,
@@ -1089,8 +1163,6 @@ const rootRouteChildren: RootRouteChildren = {
ScriptsRoute: ScriptsRoute,
StreamRoute: StreamRoute,
UsersRoute: UsersRouteWithChildren,
- Char45824Char54620Char48124Char44397Route:
- Char45824Char54620Char48124Char44397Route,
ApiUsersRoute: ApiUsersRouteWithChildren,
MultiCookieRedirectTargetRoute: MultiCookieRedirectTargetRoute,
RedirectTargetRoute: RedirectTargetRouteWithChildren,
diff --git a/e2e/vue-start/basic/src/routes/specialChars/$param.tsx b/e2e/vue-start/basic/src/routes/specialChars/$param.tsx
new file mode 100644
index 00000000000..de3cba0a97e
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/$param.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/specialChars/$param')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+
+ Hello "/specialChars/$param":{' '}
+ {params.value.param}
+
+ )
+}
diff --git a/e2e/vue-start/basic/src/routes/specialChars/route.tsx b/e2e/vue-start/basic/src/routes/specialChars/route.tsx
new file mode 100644
index 00000000000..7e31bd63fd6
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/route.tsx
@@ -0,0 +1,44 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/specialChars')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Hello "/specialChars"!
+
+ Unicode
+ {' '}
+
+ Unicode param
+ {' '}
+
+ Unicode search param
+ {' '}
+
+
+
+ )
+}
diff --git a/e2e/vue-start/basic/src/routes/specialChars/search.tsx b/e2e/vue-start/basic/src/routes/specialChars/search.tsx
new file mode 100644
index 00000000000..5ba858e7470
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/search.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/specialChars/search')({
+ validateSearch: z.object({
+ searchParam: z.string(),
+ }),
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+
+ return (
+
+ Hello "/specialChars/search"!
+
+ {search.value.searchParam}
+
+
+ )
+}
diff --git "a/e2e/vue-start/basic/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx" "b/e2e/vue-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
similarity index 54%
rename from "e2e/vue-start/basic/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
rename to "e2e/vue-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
index 16196a6bda7..90bd3120569 100644
--- "a/e2e/vue-start/basic/src/routes/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
+++ "b/e2e/vue-start/basic/src/routes/specialChars/\353\214\200\355\225\234\353\257\274\352\265\255.tsx"
@@ -1,13 +1,16 @@
import { createFileRoute } from '@tanstack/vue-router'
-export const Route = createFileRoute('/대한민국')({
+export const Route = createFileRoute('/specialChars/대한민국')({
component: KoreaComponent,
})
function KoreaComponent() {
return (
-
대한민국
+ Test
+
+ Hello /specialChars/대한민국
+
This is a route with a non-ASCII path.
)
diff --git a/e2e/vue-start/basic/tests/params.spec.ts b/e2e/vue-start/basic/tests/params.spec.ts
deleted file mode 100644
index 46ed630994c..00000000000
--- a/e2e/vue-start/basic/tests/params.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { expect } from '@playwright/test'
-
-import { test } from '@tanstack/router-e2e-utils'
-
-test.beforeEach(async ({ page }) => {
- await page.goto('/')
-})
-
-test.use({
- whitelistErrors: [
- 'Failed to load resource: the server responded with a status of 404',
- ],
-})
-test.describe('Unicode route rendering', () => {
- test('should render non-latin route correctly', async ({ page, baseURL }) => {
- await page.goto('/대한민국')
-
- await expect(page.locator('body')).toContainText('대한민국')
-
- expect(page.url()).toBe(`${baseURL}/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`)
- })
-})
diff --git a/e2e/vue-start/basic/tests/prerendering.spec.ts b/e2e/vue-start/basic/tests/prerendering.spec.ts
index 7718fe86ef0..8506ff9b061 100644
--- a/e2e/vue-start/basic/tests/prerendering.spec.ts
+++ b/e2e/vue-start/basic/tests/prerendering.spec.ts
@@ -17,7 +17,9 @@ test.describe('Prerender Static Path Discovery', () => {
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
- expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)
+ expect(
+ existsSync(join(distDir, 'specialChars/대한민국/index.html')),
+ ).toBe(true)
// Pathless layouts should NOT be prerendered (they start with _)
expect(existsSync(join(distDir, '_layout', 'index.html'))).toBe(false) // /_layout
diff --git a/e2e/vue-start/basic/tests/special-characters.spec.ts b/e2e/vue-start/basic/tests/special-characters.spec.ts
new file mode 100644
index 00000000000..b583e28fdd8
--- /dev/null
+++ b/e2e/vue-start/basic/tests/special-characters.spec.ts
@@ -0,0 +1,104 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+
+test.use({
+ whitelistErrors: [
+ /Failed to load resource: the server responded with a status of 404/,
+ ],
+})
+test.describe('Unicode route rendering', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/specialChars')
+ })
+
+ test('should render non-latin route correctly with direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/대한민국')
+ await page.waitForURL(
+ `${baseURL}/specialChars/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`,
+ )
+
+ await expect(page.getByTestId('special-non-latin-heading')).toBeInViewport()
+ })
+
+ test('should render non-latin route correctly during router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const nonLatinLink = page.getByTestId('special-non-latin-link')
+
+ await nonLatinLink.click()
+ await page.waitForURL(
+ `${baseURL}/specialChars/%EB%8C%80%ED%95%9C%EB%AF%BC%EA%B5%AD`,
+ )
+
+ await expect(page.getByTestId('special-non-latin-heading')).toBeInViewport()
+ })
+
+ test.describe('Special characters in path params', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/대|')
+ await page.waitForURL(`${baseURL}/specialChars/%EB%8C%80%7C`)
+
+ const param = await page.getByTestId('special-param').textContent()
+
+ expect(param).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const link = page.getByTestId('special-param-link')
+
+ await link.click()
+ await page.waitForURL(`${baseURL}/specialChars/%EB%8C%80%7C`)
+
+ const param = await page.getByTestId('special-param').textContent()
+
+ expect(param).toBe('대|')
+ })
+ })
+
+ test.describe('Special characters in search params', () => {
+ test('should render route correctly on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/search?searchParam=대|')
+
+ await page.waitForURL(
+ `${baseURL}/specialChars/search?searchParam=%EB%8C%80|`,
+ )
+
+ const searchParam = await page
+ .getByTestId('special-search-param')
+ .textContent()
+
+ expect(searchParam).toBe('대|')
+ })
+
+ test('should render route correctly on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const link = page.getByTestId('special-searchParam-link')
+
+ await link.click()
+ await page.waitForURL(
+ `${baseURL}/specialChars/search?searchParam=%EB%8C%80%7C`,
+ )
+
+ const searchParam = await page
+ .getByTestId('special-search-param')
+ .textContent()
+
+ expect(searchParam).toBe('대|')
+ })
+ })
+})
diff --git a/e2e/vue-start/basic/vite.config.ts b/e2e/vue-start/basic/vite.config.ts
index 96e38ba6ca8..58f6baaab6a 100644
--- a/e2e/vue-start/basic/vite.config.ts
+++ b/e2e/vue-start/basic/vite.config.ts
@@ -22,6 +22,7 @@ const prerenderConfiguration = {
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
+ '/specialChars/search',
'/search-params', // search-param routes have dynamic content based on query params
'/transition',
'/users',
diff --git a/e2e/vue-start/virtual-routes/routes.ts b/e2e/vue-start/virtual-routes/routes.ts
index ab17b8f58b4..37573a05671 100644
--- a/e2e/vue-start/virtual-routes/routes.ts
+++ b/e2e/vue-start/virtual-routes/routes.ts
@@ -21,4 +21,5 @@ export const routes = rootRoute('root.tsx', [
]),
]),
physical('/classic', 'file-based-subtree'),
+ route('/special|pipe', 'pipe.tsx'),
])
diff --git a/e2e/vue-start/virtual-routes/src/routeTree.gen.ts b/e2e/vue-start/virtual-routes/src/routeTree.gen.ts
index 1ec4fba15f1..addf953a25d 100644
--- a/e2e/vue-start/virtual-routes/src/routeTree.gen.ts
+++ b/e2e/vue-start/virtual-routes/src/routeTree.gen.ts
@@ -9,6 +9,7 @@
// 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 pipeRouteImport } from './routes/pipe'
import { Route as postsPostsRouteImport } from './routes/posts/posts'
import { Route as layoutFirstLayoutRouteImport } from './routes/layout/first-layout'
import { Route as homeRouteImport } from './routes/home'
@@ -22,6 +23,11 @@ import { Route as ClassicHelloUniverseRouteImport } from './routes/file-based-su
import { Route as bRouteImport } from './routes/b'
import { Route as aRouteImport } from './routes/a'
+const pipeRoute = pipeRouteImport.update({
+ id: '/special|pipe',
+ path: '/special|pipe',
+ getParentRoute: () => rootRouteImport,
+} as any)
const postsPostsRoute = postsPostsRouteImport.update({
id: '/posts',
path: '/posts',
@@ -84,6 +90,7 @@ const aRoute = aRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof homeRoute
'/posts': typeof postsPostsRouteWithChildren
+ '/special|pipe': typeof pipeRoute
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
'/posts/': typeof postsPostsHomeRoute
'/posts/$postId': typeof postsPostsDetailRoute
@@ -95,6 +102,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof homeRoute
+ '/special|pipe': typeof pipeRoute
'/posts': typeof postsPostsHomeRoute
'/posts/$postId': typeof postsPostsDetailRoute
'/classic/hello/universe': typeof ClassicHelloUniverseRoute
@@ -108,6 +116,7 @@ export interface FileRoutesById {
'/': typeof homeRoute
'/_first': typeof layoutFirstLayoutRouteWithChildren
'/posts': typeof postsPostsRouteWithChildren
+ '/special|pipe': typeof pipeRoute
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
'/posts/': typeof postsPostsHomeRoute
'/_first/_second-layout': typeof layoutSecondLayoutRouteWithChildren
@@ -123,6 +132,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/posts'
+ | '/special|pipe'
| '/classic/hello'
| '/posts/'
| '/posts/$postId'
@@ -134,6 +144,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/special|pipe'
| '/posts'
| '/posts/$postId'
| '/classic/hello/universe'
@@ -146,6 +157,7 @@ export interface FileRouteTypes {
| '/'
| '/_first'
| '/posts'
+ | '/special|pipe'
| '/classic/hello'
| '/posts/'
| '/_first/_second-layout'
@@ -161,11 +173,19 @@ export interface RootRouteChildren {
homeRoute: typeof homeRoute
layoutFirstLayoutRoute: typeof layoutFirstLayoutRouteWithChildren
postsPostsRoute: typeof postsPostsRouteWithChildren
+ pipeRoute: typeof pipeRoute
ClassicHelloRouteRoute: typeof ClassicHelloRouteRouteWithChildren
}
declare module '@tanstack/vue-router' {
interface FileRoutesByPath {
+ '/special|pipe': {
+ id: '/special|pipe'
+ path: '/special|pipe'
+ fullPath: '/special|pipe'
+ preLoaderRoute: typeof pipeRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/posts': {
id: '/posts'
path: '/posts'
@@ -310,6 +330,7 @@ const rootRouteChildren: RootRouteChildren = {
homeRoute: homeRoute,
layoutFirstLayoutRoute: layoutFirstLayoutRouteWithChildren,
postsPostsRoute: postsPostsRouteWithChildren,
+ pipeRoute: pipeRoute,
ClassicHelloRouteRoute: ClassicHelloRouteRouteWithChildren,
}
export const routeTree = rootRouteImport
diff --git a/e2e/vue-start/virtual-routes/src/routes/pipe.tsx b/e2e/vue-start/virtual-routes/src/routes/pipe.tsx
new file mode 100644
index 00000000000..bd7dd29a64a
--- /dev/null
+++ b/e2e/vue-start/virtual-routes/src/routes/pipe.tsx
@@ -0,0 +1,11 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/special|pipe')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+ Hello "/special|pipe"!
+ )
+}
diff --git a/e2e/vue-start/virtual-routes/src/routes/root.tsx b/e2e/vue-start/virtual-routes/src/routes/root.tsx
index 2a3a8b98cb8..8193fcea71a 100644
--- a/e2e/vue-start/virtual-routes/src/routes/root.tsx
+++ b/e2e/vue-start/virtual-routes/src/routes/root.tsx
@@ -69,6 +69,15 @@ function RootComponent() {
>
Subtree
{' '}
+
+ Pipe
+ {' '}
{
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/')
+ await page.waitForURL('/')
+ })
+
+ test.describe('Special characters in route paths', () => {
+ test('should render route with pipe character in path on direct navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/special|pipe')
+ await page.waitForURL(`${baseURL}/special%7Cpipe`)
+
+ await expect(
+ page.getByTestId('special-pipe-route-heading'),
+ ).toBeInViewport()
+ })
+
+ test('should render route with pipe character in path on router navigation', async ({
+ page,
+ baseURL,
+ }) => {
+ const pipeLink = page.getByTestId('special-pipe-link')
+
+ await pipeLink.click()
+ await page.waitForURL(`${baseURL}/special%7Cpipe`)
+
+ await expect(
+ page.getByTestId('special-pipe-route-heading'),
+ ).toBeInViewport()
+ })
+ })
+})
diff --git a/packages/router-core/src/ssr/createRequestHandler.ts b/packages/router-core/src/ssr/createRequestHandler.ts
index 29b7c1e25d9..53e3a94e7c4 100644
--- a/packages/router-core/src/ssr/createRequestHandler.ts
+++ b/packages/router-core/src/ssr/createRequestHandler.ts
@@ -1,6 +1,10 @@
import { createMemoryHistory } from '@tanstack/history'
import { mergeHeaders } from './headers'
-import { attachRouterServerSsrUtils, getOrigin } from './ssr-server'
+import {
+ attachRouterServerSsrUtils,
+ getNormalizedURL,
+ getOrigin,
+} from './ssr-server'
import type { HandlerCallback } from './handlerCallback'
import type { AnyRouter } from '../router'
import type { Manifest } from '../manifest'
@@ -29,7 +33,8 @@ export function createRequestHandler({
manifest: await getRouterManifest?.(),
})
- const url = new URL(request.url, 'http://localhost')
+ // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.
+ const url = getNormalizedURL(request.url, 'http://localhost')
const origin = getOrigin(request)
const href = url.href.replace(url.origin, '')
diff --git a/packages/router-core/src/ssr/server.ts b/packages/router-core/src/ssr/server.ts
index de39fb81367..89b6059e6de 100644
--- a/packages/router-core/src/ssr/server.ts
+++ b/packages/router-core/src/ssr/server.ts
@@ -7,4 +7,8 @@ export {
transformStreamWithRouter,
transformReadableStreamWithRouter,
} from './transformStreamWithRouter'
-export { attachRouterServerSsrUtils, getOrigin } from './ssr-server'
+export {
+ attachRouterServerSsrUtils,
+ getNormalizedURL,
+ getOrigin,
+} from './ssr-server'
diff --git a/packages/router-core/src/ssr/ssr-server.ts b/packages/router-core/src/ssr/ssr-server.ts
index 254d2a0f1e1..81af3898f6c 100644
--- a/packages/router-core/src/ssr/ssr-server.ts
+++ b/packages/router-core/src/ssr/ssr-server.ts
@@ -1,5 +1,6 @@
import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'
import invariant from 'tiny-invariant'
+import { decodePath } from '../utils'
import minifiedTsrBootStrapScript from './tsrScript?script-string'
import { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'
import { defaultSerovalPlugins } from './serializer/seroval-plugins'
@@ -348,3 +349,22 @@ export function getOrigin(request: Request) {
} catch {}
return 'http://localhost'
}
+
+// server and browser can decode/encode characters differently in paths and search params.
+// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.
+// for example, in paths "|" is not encoded on the server but is encoded on chromium (and not on firefox) while "대" is encoded on both sides.
+// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.
+// new URLSearchParams() encodes "|" while new URL() does not, and in this instance
+// chromium treats search params differently than paths, i.e. "|" is not encoded in search params.
+export function getNormalizedURL(url: string | URL, base?: string | URL) {
+ const rawUrl = new URL(url, base)
+ const decodedPathname = decodePath(rawUrl.pathname)
+ const searchParams = new URLSearchParams(rawUrl.search)
+ const normalizedHref =
+ decodedPathname +
+ (searchParams.size > 0 ? '?' : '') +
+ searchParams.toString() +
+ rawUrl.hash
+
+ return new URL(normalizedHref, rawUrl.origin)
+}
diff --git a/packages/router-core/tests/getNormalizedURL.test.ts b/packages/router-core/tests/getNormalizedURL.test.ts
new file mode 100644
index 00000000000..24fb6bd7bca
--- /dev/null
+++ b/packages/router-core/tests/getNormalizedURL.test.ts
@@ -0,0 +1,68 @@
+import { describe, expect, test } from 'vitest'
+import { getNormalizedURL } from '../src/ssr/ssr-server'
+
+describe('getNormalizedURL', () => {
+ test('should return URL that is in standardized format', () => {
+ const url1 = 'https://example.com/%EB%8C%80%7C/path?query=%EB%8C%80|#hash'
+ const url2 = 'https://example.com/%EB%8C%80|/path?query=%EB%8C%80%7C#hash'
+
+ const normalizedUrl1 = getNormalizedURL(url1)
+ const normalizedUrl2 = getNormalizedURL(url2)
+
+ expect(normalizedUrl1.pathname).toBe('/%EB%8C%80|/path')
+ expect(normalizedUrl1.pathname).toBe(normalizedUrl2.pathname)
+ expect(new URL(url1).pathname).not.toBe(new URL(url2).pathname)
+
+ expect(normalizedUrl1.search).toBe(`?query=%EB%8C%80%7C`)
+ expect(normalizedUrl1.search).toBe(normalizedUrl2.search)
+ expect(new URL(url1).search).not.toBe(new URL(url2).search)
+ })
+
+ const testCases = [
+ {
+ url: 'https://example.com/%3Fstart?query=value',
+ expectedPathName: '/%3Fstart',
+ expectedSearchParams: '?query=value',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/end%3F?query=value',
+ expectedPathName: '/end%3F',
+ expectedSearchParams: '?query=value',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/%23?query=value',
+ expectedPathName: '/%23',
+ expectedSearchParams: '?query=value',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/a%3Fb%3Fc%23d?query=value',
+ expectedPathName: '/a%3Fb%3Fc%23d',
+ expectedSearchParams: '?query=value',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path?query=value#section%3Fpart',
+ expectedPathName: '/path',
+ expectedSearchParams: '?query=value',
+ expectedHash: '#section%3Fpart',
+ },
+ {
+ url: 'https://example.com/start%3Fmiddle%23end?key=value%23part&other=%3Fdata#section%3Fpart',
+ expectedPathName: '/start%3Fmiddle%23end',
+ expectedSearchParams: '?key=value%23part&other=%3Fdata',
+ expectedHash: '#section%3Fpart',
+ },
+ ]
+ test.each(testCases)(
+ 'should treat encoded URL specific characters correctly',
+ ({ url, expectedPathName, expectedHash, expectedSearchParams }) => {
+ const normalizedUrl = getNormalizedURL(url)
+ expect(normalizedUrl.pathname).toBe(expectedPathName)
+ expect(normalizedUrl.search).toBe(expectedSearchParams)
+ expect(normalizedUrl.hash).toBe(expectedHash)
+ },
+ )
+})
diff --git a/packages/start-server-core/src/createStartHandler.ts b/packages/start-server-core/src/createStartHandler.ts
index c35a24f3092..9944267d18f 100644
--- a/packages/start-server-core/src/createStartHandler.ts
+++ b/packages/start-server-core/src/createStartHandler.ts
@@ -12,6 +12,7 @@ import {
} from '@tanstack/router-core'
import {
attachRouterServerSsrUtils,
+ getNormalizedURL,
getOrigin,
} from '@tanstack/router-core/ssr/server'
import { runWithStartContext } from '@tanstack/start-storage-context'
@@ -216,7 +217,8 @@ export function createStartHandler(
let cbWillCleanup = false as boolean
try {
- const url = new URL(request.url)
+ // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.
+ const url = getNormalizedURL(request.url)
const href = url.href.replace(url.origin, '')
const origin = getOrigin(request)