diff --git a/e2e/react-start/basic/package.json b/e2e/react-start/basic/package.json
index 89106b02538..ffcd8cc63ca 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 21841bff704..1477ed0bac3 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'
@@ -47,7 +50,10 @@ import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/vi
import { Route as MultiCookieRedirectTargetRouteImport } from './routes/multi-cookie-redirect/target'
import { Route as ApiUsersRouteImport } from './routes/api.users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as SpecialCharsMalformedRouteRouteImport } from './routes/specialChars/malformed/route'
import { Route as RedirectTargetIndexRouteImport } from './routes/redirect/$target/index'
+import { Route as SpecialCharsMalformedSearchRouteImport } from './routes/specialChars/malformed/search'
+import { Route as SpecialCharsMalformedParamRouteImport } from './routes/specialChars/malformed/$param'
import { Route as RedirectTargetViaLoaderRouteImport } from './routes/redirect/$target/via-loader'
import { Route as RedirectTargetViaBeforeLoadRouteImport } from './routes/redirect/$target/via-beforeLoad'
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
@@ -61,12 +67,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 +121,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 +182,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',
@@ -253,11 +274,29 @@ const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
} as any)
+const SpecialCharsMalformedRouteRoute =
+ SpecialCharsMalformedRouteRouteImport.update({
+ id: '/malformed',
+ path: '/malformed',
+ getParentRoute: () => SpecialCharsRouteRoute,
+ } as any)
const RedirectTargetIndexRoute = RedirectTargetIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => RedirectTargetRoute,
} as any)
+const SpecialCharsMalformedSearchRoute =
+ SpecialCharsMalformedSearchRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => SpecialCharsMalformedRouteRoute,
+ } as any)
+const SpecialCharsMalformedParamRoute =
+ SpecialCharsMalformedParamRouteImport.update({
+ id: '/$param',
+ path: '/$param',
+ getParentRoute: () => SpecialCharsMalformedRouteRoute,
+ } as any)
const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderRouteImport.update({
id: '/via-loader',
path: '/via-loader',
@@ -328,6 +367,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 +378,7 @@ export interface FileRoutesByFullPath {
'/stream': typeof StreamRoute
'/type-only-reexport': typeof TypeOnlyReexportRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -353,6 +393,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
@@ -367,6 +410,8 @@ export interface FileRoutesByFullPath {
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/foo/$bar/$qux': typeof FooBarQuxHereRouteWithChildren
'/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
@@ -377,6 +422,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 +430,7 @@ export interface FileRoutesByTo {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/type-only-reexport': typeof TypeOnlyReexportRoute
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -398,6 +444,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
@@ -412,6 +461,8 @@ export interface FileRoutesByTo {
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/redirect/$target': typeof RedirectTargetIndexRoute
'/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
'/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
@@ -424,6 +475,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 +487,7 @@ export interface FileRoutesById {
'/stream': typeof StreamRoute
'/type-only-reexport': typeof TypeOnlyReexportRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
@@ -451,6 +503,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
@@ -465,6 +520,8 @@ export interface FileRoutesById {
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/foo/$bar/$qux/_here': typeof FooBarQuxHereRouteWithChildren
'/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
@@ -479,6 +536,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/client-only'
| '/deferred'
| '/inline-scripts'
@@ -489,7 +547,7 @@ export interface FileRouteTypes {
| '/stream'
| '/type-only-reexport'
| '/users'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -504,6 +562,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/'
@@ -518,6 +579,8 @@ export interface FileRouteTypes {
| '/posts/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/redirect/$target/'
| '/foo/$bar/$qux'
| '/redirect/$target/serverFn/via-beforeLoad'
@@ -528,6 +591,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/specialChars'
| '/client-only'
| '/deferred'
| '/inline-scripts'
@@ -535,7 +599,7 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/type-only-reexport'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -549,6 +613,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'
@@ -563,6 +630,8 @@ export interface FileRouteTypes {
| '/posts/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/redirect/$target'
| '/redirect/$target/serverFn/via-beforeLoad'
| '/redirect/$target/serverFn/via-loader'
@@ -574,6 +643,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/_layout'
| '/client-only'
| '/deferred'
@@ -585,7 +655,7 @@ export interface FileRouteTypes {
| '/stream'
| '/type-only-reexport'
| '/users'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/_layout/_layout-2'
| '/api/users'
| '/multi-cookie-redirect/target'
@@ -601,6 +671,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/'
@@ -615,6 +688,8 @@ export interface FileRouteTypes {
| '/posts_/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/redirect/$target/'
| '/foo/$bar/$qux/_here'
| '/redirect/$target/serverFn/via-beforeLoad'
@@ -628,6 +703,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 +715,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 +726,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 +803,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 +887,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'
@@ -917,6 +1013,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutLayout2RouteImport
parentRoute: typeof LayoutRoute
}
+ '/specialChars/malformed': {
+ id: '/specialChars/malformed'
+ path: '/malformed'
+ fullPath: '/specialChars/malformed'
+ preLoaderRoute: typeof SpecialCharsMalformedRouteRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/redirect/$target/': {
id: '/redirect/$target/'
path: '/'
@@ -924,6 +1027,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof RedirectTargetIndexRouteImport
parentRoute: typeof RedirectTargetRoute
}
+ '/specialChars/malformed/search': {
+ id: '/specialChars/malformed/search'
+ path: '/search'
+ fullPath: '/specialChars/malformed/search'
+ preLoaderRoute: typeof SpecialCharsMalformedSearchRouteImport
+ parentRoute: typeof SpecialCharsMalformedRouteRoute
+ }
+ '/specialChars/malformed/$param': {
+ id: '/specialChars/malformed/$param'
+ path: '/$param'
+ fullPath: '/specialChars/malformed/$param'
+ preLoaderRoute: typeof SpecialCharsMalformedParamRouteImport
+ parentRoute: typeof SpecialCharsMalformedRouteRoute
+ }
'/redirect/$target/via-loader': {
id: '/redirect/$target/via-loader'
path: '/via-loader'
@@ -1042,6 +1159,40 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface SpecialCharsMalformedRouteRouteChildren {
+ SpecialCharsMalformedParamRoute: typeof SpecialCharsMalformedParamRoute
+ SpecialCharsMalformedSearchRoute: typeof SpecialCharsMalformedSearchRoute
+}
+
+const SpecialCharsMalformedRouteRouteChildren: SpecialCharsMalformedRouteRouteChildren =
+ {
+ SpecialCharsMalformedParamRoute: SpecialCharsMalformedParamRoute,
+ SpecialCharsMalformedSearchRoute: SpecialCharsMalformedSearchRoute,
+ }
+
+const SpecialCharsMalformedRouteRouteWithChildren =
+ SpecialCharsMalformedRouteRoute._addFileChildren(
+ SpecialCharsMalformedRouteRouteChildren,
+ )
+
+interface SpecialCharsRouteRouteChildren {
+ SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
+ SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
+ SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
+}
+
+const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
+ SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
+ SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsSearchRoute: SpecialCharsSearchRoute,
+ SpecialCharsChar45824Char54620Char48124Char44397Route:
+ SpecialCharsChar45824Char54620Char48124Char44397Route,
+}
+
+const SpecialCharsRouteRouteWithChildren =
+ SpecialCharsRouteRoute._addFileChildren(SpecialCharsRouteRouteChildren)
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -1169,6 +1320,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ SpecialCharsRouteRoute: SpecialCharsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
ClientOnlyRoute: ClientOnlyRoute,
DeferredRoute: DeferredRoute,
@@ -1180,8 +1332,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/malformed/$param.tsx b/e2e/react-start/basic/src/routes/specialChars/malformed/$param.tsx
new file mode 100644
index 00000000000..4040b6b7d45
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/malformed/$param.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/specialChars/malformed/$param')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const { param } = Route.useParams()
+ return (
+
+ Hello "/specialChars/malformed/$param":{' '}
+ {param}
+
+ )
+}
diff --git a/e2e/react-start/basic/src/routes/specialChars/malformed/route.tsx b/e2e/react-start/basic/src/routes/specialChars/malformed/route.tsx
new file mode 100644
index 00000000000..91114692b2b
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/malformed/route.tsx
@@ -0,0 +1,29 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/specialChars/malformed')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Hello "/specialChars/malformed"!
+
+ malformed path param
+ {' '}
+
+ malformed search param
+ {' '}
+
+
+
+ )
+}
diff --git a/e2e/react-start/basic/src/routes/specialChars/malformed/search.tsx b/e2e/react-start/basic/src/routes/specialChars/malformed/search.tsx
new file mode 100644
index 00000000000..c5256de3372
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/malformed/search.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute } from '@tanstack/react-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/specialChars/malformed/search')({
+ validateSearch: z.object({
+ searchParam: z.string(),
+ }),
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+
+ return (
+
+ Hello "/specialChars/malformed/search"!
+
+ {search.searchParam}
+
+
+ )
+}
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..a6069cff481
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/specialChars/route.tsx
@@ -0,0 +1,53 @@
+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
+ {' '}
+
+ Malformed paths
+ {' '}
+
+
+
+ )
+}
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..b09942ce5d5
--- /dev/null
+++ b/e2e/react-start/basic/tests/special-characters.spec.ts
@@ -0,0 +1,172 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import { isSpaMode } from './utils/isSpaMode'
+
+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('대|')
+ })
+ })
+
+ test.describe('malformed paths', () => {
+ test.use({
+ whitelistErrors: [
+ 'Failed to load resource: the server responded with a status of 404',
+ 'Failed to load resource: the server responded with a status of 400 (Bad Request)',
+ ],
+ })
+
+ test('un-matched malformed paths should return not found on direct navigation', async ({
+ page,
+ }) => {
+ const res = await page.goto('/specialChars/malformed/%E0%A4')
+
+ await page.waitForLoadState(`load`)
+
+ // in spa mode this is caught and handled at server level
+ if (!isSpaMode) {
+ expect(res!.status()).toBe(404)
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ } else {
+ expect(res!.status()).toBe(400)
+ }
+ })
+
+ test('malformed path params should return not found on router link', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/malformed')
+ await page.waitForURL(`${baseURL}/specialChars/malformed`)
+
+ const link = page.getByTestId('special-malformed-path-link')
+
+ await link.click()
+
+ await page.waitForLoadState('load')
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ })
+
+ test('un-matched malformed paths should return not found on direct navigation in search params', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/malformed/search?searchParam=%E0%A4')
+
+ await page.waitForURL(
+ `${baseURL}/specialChars/malformed/search?searchParam=%E0%A4`,
+ )
+
+ await expect(
+ page.getByTestId('special-malformed-search-param'),
+ ).toBeInViewport()
+
+ const searchParam = await page
+ .getByTestId('special-malformed-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..f0a0fe6a1b5 100644
--- a/e2e/react-start/basic/vite.config.ts
+++ b/e2e/react-start/basic/vite.config.ts
@@ -22,6 +22,8 @@ const prerenderConfiguration = {
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
+ '/specialChars/search',
+ '/specialChars/malformed',
'/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 f32d59348ce..83b85e36fdf 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..70ae25782e3 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'
@@ -45,9 +48,12 @@ import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/vi
import { Route as MultiCookieRedirectTargetRouteImport } from './routes/multi-cookie-redirect/target'
import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as SpecialCharsMalformedRouteRouteImport } from './routes/specialChars/malformed/route'
import { Route as RedirectTargetIndexRouteImport } from './routes/redirect/$target/index'
import { Route as TransitionTypingCreateResourceRouteImport } from './routes/transition/typing/create-resource'
import { Route as TransitionCountCreateResourceRouteImport } from './routes/transition/count/create-resource'
+import { Route as SpecialCharsMalformedSearchRouteImport } from './routes/specialChars/malformed/search'
+import { Route as SpecialCharsMalformedParamRouteImport } from './routes/specialChars/malformed/$param'
import { Route as RedirectTargetViaLoaderRouteImport } from './routes/redirect/$target/via-loader'
import { Route as RedirectTargetViaBeforeLoadRouteImport } from './routes/redirect/$target/via-beforeLoad'
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
@@ -59,12 +65,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 +109,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 +170,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',
@@ -241,6 +262,12 @@ const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
} as any)
+const SpecialCharsMalformedRouteRoute =
+ SpecialCharsMalformedRouteRouteImport.update({
+ id: '/malformed',
+ path: '/malformed',
+ getParentRoute: () => SpecialCharsRouteRoute,
+ } as any)
const RedirectTargetIndexRoute = RedirectTargetIndexRouteImport.update({
id: '/',
path: '/',
@@ -258,6 +285,18 @@ const TransitionCountCreateResourceRoute =
path: '/transition/count/create-resource',
getParentRoute: () => rootRouteImport,
} as any)
+const SpecialCharsMalformedSearchRoute =
+ SpecialCharsMalformedSearchRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => SpecialCharsMalformedRouteRoute,
+ } as any)
+const SpecialCharsMalformedParamRoute =
+ SpecialCharsMalformedParamRouteImport.update({
+ id: '/$param',
+ path: '/$param',
+ getParentRoute: () => SpecialCharsMalformedRouteRoute,
+ } as any)
const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderRouteImport.update({
id: '/via-loader',
path: '/via-loader',
@@ -318,6 +357,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 +366,7 @@ export interface FileRoutesByFullPath {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -341,12 +381,15 @@ 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
+ '/multi-cookie-redirect/': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
'/posts/': typeof PostsIndexRoute
'/raw-stream/': typeof RawStreamIndexRoute
- '/redirect': typeof RedirectIndexRoute
+ '/redirect/': typeof RedirectIndexRoute
'/search-params/': typeof SearchParamsIndexRoute
'/users/': typeof UsersIndexRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
@@ -355,22 +398,25 @@ export interface FileRoutesByFullPath {
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/transition/count/create-resource': typeof TransitionCountCreateResourceRoute
'/transition/typing/create-resource': typeof TransitionTypingCreateResourceRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
'/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
'/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
- '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
+ '/redirect/$target/serverFn/': typeof RedirectTargetServerFnIndexRoute
}
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
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -384,6 +430,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
@@ -398,6 +447,8 @@ export interface FileRoutesByTo {
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/transition/count/create-resource': typeof TransitionCountCreateResourceRoute
'/transition/typing/create-resource': typeof TransitionTypingCreateResourceRoute
'/redirect/$target': typeof RedirectTargetIndexRoute
@@ -411,6 +462,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 +472,7 @@ export interface FileRoutesById {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
@@ -436,6 +488,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
@@ -450,6 +505,8 @@ export interface FileRoutesById {
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/transition/count/create-resource': typeof TransitionCountCreateResourceRoute
'/transition/typing/create-resource': typeof TransitionTypingCreateResourceRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
@@ -464,6 +521,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
@@ -472,7 +530,7 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -487,12 +545,15 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
- | '/multi-cookie-redirect'
+ | '/multi-cookie-redirect/'
| '/not-found/'
| '/posts/'
| '/raw-stream/'
- | '/redirect'
+ | '/redirect/'
| '/search-params/'
| '/users/'
| '/layout-a'
@@ -501,22 +562,25 @@ export interface FileRouteTypes {
| '/posts/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/transition/count/create-resource'
| '/transition/typing/create-resource'
| '/redirect/$target/'
| '/redirect/$target/serverFn/via-beforeLoad'
| '/redirect/$target/serverFn/via-loader'
| '/redirect/$target/serverFn/via-useServerFn'
- | '/redirect/$target/serverFn'
+ | '/redirect/$target/serverFn/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
| '/scripts'
| '/stream'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -530,6 +594,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'
@@ -544,6 +611,8 @@ export interface FileRouteTypes {
| '/posts/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/transition/count/create-resource'
| '/transition/typing/create-resource'
| '/redirect/$target'
@@ -556,6 +625,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/_layout'
| '/deferred'
| '/inline-scripts'
@@ -565,7 +635,7 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/_layout/_layout-2'
| '/api/users'
| '/multi-cookie-redirect/target'
@@ -581,6 +651,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/'
@@ -595,6 +668,8 @@ export interface FileRouteTypes {
| '/posts_/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/transition/count/create-resource'
| '/transition/typing/create-resource'
| '/redirect/$target/'
@@ -608,6 +683,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 +693,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 +705,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'
@@ -696,10 +764,17 @@ declare module '@tanstack/solid-router' {
'/_layout': {
id: '/_layout'
path: ''
- fullPath: ''
+ fullPath: '/'
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'
@@ -738,7 +813,7 @@ declare module '@tanstack/solid-router' {
'/redirect/': {
id: '/redirect/'
path: '/redirect'
- fullPath: '/redirect'
+ fullPath: '/redirect/'
preLoaderRoute: typeof RedirectIndexRouteImport
parentRoute: typeof rootRouteImport
}
@@ -766,7 +841,7 @@ declare module '@tanstack/solid-router' {
'/multi-cookie-redirect/': {
id: '/multi-cookie-redirect/'
path: '/multi-cookie-redirect'
- fullPath: '/multi-cookie-redirect'
+ fullPath: '/multi-cookie-redirect/'
preLoaderRoute: typeof MultiCookieRedirectIndexRouteImport
parentRoute: typeof rootRouteImport
}
@@ -777,6 +852,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'
@@ -878,10 +974,17 @@ declare module '@tanstack/solid-router' {
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
- fullPath: ''
+ fullPath: '/'
preLoaderRoute: typeof LayoutLayout2RouteImport
parentRoute: typeof LayoutRoute
}
+ '/specialChars/malformed': {
+ id: '/specialChars/malformed'
+ path: '/malformed'
+ fullPath: '/specialChars/malformed'
+ preLoaderRoute: typeof SpecialCharsMalformedRouteRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/redirect/$target/': {
id: '/redirect/$target/'
path: '/'
@@ -903,6 +1006,20 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof TransitionCountCreateResourceRouteImport
parentRoute: typeof rootRouteImport
}
+ '/specialChars/malformed/search': {
+ id: '/specialChars/malformed/search'
+ path: '/search'
+ fullPath: '/specialChars/malformed/search'
+ preLoaderRoute: typeof SpecialCharsMalformedSearchRouteImport
+ parentRoute: typeof SpecialCharsMalformedRouteRoute
+ }
+ '/specialChars/malformed/$param': {
+ id: '/specialChars/malformed/$param'
+ path: '/$param'
+ fullPath: '/specialChars/malformed/$param'
+ preLoaderRoute: typeof SpecialCharsMalformedParamRouteImport
+ parentRoute: typeof SpecialCharsMalformedRouteRoute
+ }
'/redirect/$target/via-loader': {
id: '/redirect/$target/via-loader'
path: '/via-loader'
@@ -948,7 +1065,7 @@ declare module '@tanstack/solid-router' {
'/redirect/$target/serverFn/': {
id: '/redirect/$target/serverFn/'
path: '/serverFn'
- fullPath: '/redirect/$target/serverFn'
+ fullPath: '/redirect/$target/serverFn/'
preLoaderRoute: typeof RedirectTargetServerFnIndexRouteImport
parentRoute: typeof RedirectTargetRoute
}
@@ -1007,6 +1124,40 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface SpecialCharsMalformedRouteRouteChildren {
+ SpecialCharsMalformedParamRoute: typeof SpecialCharsMalformedParamRoute
+ SpecialCharsMalformedSearchRoute: typeof SpecialCharsMalformedSearchRoute
+}
+
+const SpecialCharsMalformedRouteRouteChildren: SpecialCharsMalformedRouteRouteChildren =
+ {
+ SpecialCharsMalformedParamRoute: SpecialCharsMalformedParamRoute,
+ SpecialCharsMalformedSearchRoute: SpecialCharsMalformedSearchRoute,
+ }
+
+const SpecialCharsMalformedRouteRouteWithChildren =
+ SpecialCharsMalformedRouteRoute._addFileChildren(
+ SpecialCharsMalformedRouteRouteChildren,
+ )
+
+interface SpecialCharsRouteRouteChildren {
+ SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
+ SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
+ SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
+}
+
+const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
+ SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
+ SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsSearchRoute: SpecialCharsSearchRoute,
+ SpecialCharsChar45824Char54620Char48124Char44397Route:
+ SpecialCharsChar45824Char54620Char48124Char44397Route,
+}
+
+const SpecialCharsRouteRouteWithChildren =
+ SpecialCharsRouteRoute._addFileChildren(SpecialCharsRouteRouteChildren)
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -1122,6 +1273,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ SpecialCharsRouteRoute: SpecialCharsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
DeferredRoute: DeferredRoute,
InlineScriptsRoute: InlineScriptsRoute,
@@ -1131,8 +1283,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/malformed/$param.tsx b/e2e/solid-start/basic/src/routes/specialChars/malformed/$param.tsx
new file mode 100644
index 00000000000..132d7f72bf2
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/malformed/$param.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/specialChars/malformed/$param')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+
+ Hello "/specialChars/malformed/$param":{' '}
+ {params().param}
+
+ )
+}
diff --git a/e2e/solid-start/basic/src/routes/specialChars/malformed/route.tsx b/e2e/solid-start/basic/src/routes/specialChars/malformed/route.tsx
new file mode 100644
index 00000000000..1b32e5ff710
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/malformed/route.tsx
@@ -0,0 +1,29 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/specialChars/malformed')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Hello "/specialChars/malformed"!
+
+ malformed path param
+ {' '}
+
+ malformed search param
+ {' '}
+
+
+
+ )
+}
diff --git a/e2e/solid-start/basic/src/routes/specialChars/malformed/search.tsx b/e2e/solid-start/basic/src/routes/specialChars/malformed/search.tsx
new file mode 100644
index 00000000000..a9aea491370
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/malformed/search.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/specialChars/malformed/search')({
+ validateSearch: z.object({
+ searchParam: z.string(),
+ }),
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+
+ return (
+
+ Hello "/specialChars/malformed/search"!
+
+ {search().searchParam}
+
+
+ )
+}
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..0bfad91cd77
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/specialChars/route.tsx
@@ -0,0 +1,53 @@
+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
+ {' '}
+
+ Malformed paths
+ {' '}
+
+
+
+ )
+}
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..8ff7e7762e6
--- /dev/null
+++ b/e2e/solid-start/basic/tests/special-characters.spec.ts
@@ -0,0 +1,171 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import { isSpaMode } from './utils/isSpaMode'
+
+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('대|')
+ })
+ })
+
+ test.describe('malformed paths', () => {
+ test.use({
+ whitelistErrors: [
+ 'Failed to load resource: the server responded with a status of 404',
+ 'Failed to load resource: the server responded with a status of 400 (Bad Request)',
+ ],
+ })
+
+ test('un-matched malformed paths should return not found on direct navigation', async ({
+ page,
+ }) => {
+ const res = await page.goto('/specialChars/malformed/%E0%A4')
+
+ await page.waitForLoadState(`load`)
+
+ // in spa mode this is caught and handled at server level
+ if (!isSpaMode) {
+ expect(res!.status()).toBe(404)
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ } else {
+ expect(res!.status()).toBe(400)
+ }
+ })
+
+ test('malformed path params should return not found on router link', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/malformed')
+ await page.waitForURL(`${baseURL}/specialChars/malformed`)
+
+ const link = page.getByTestId('special-malformed-path-link')
+
+ await link.click()
+ await page.waitForLoadState('load')
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ })
+
+ test('un-matched malformed paths should return not found on direct navigation in search params', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/malformed/search?searchParam=%E0%A4')
+
+ await page.waitForURL(
+ `${baseURL}/specialChars/malformed/search?searchParam=%E0%A4`,
+ )
+
+ await expect(
+ page.getByTestId('special-malformed-search-param'),
+ ).toBeInViewport()
+
+ const searchParam = await page
+ .getByTestId('special-malformed-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..1301ae91393 100644
--- a/e2e/solid-start/basic/vite.config.ts
+++ b/e2e/solid-start/basic/vite.config.ts
@@ -22,6 +22,8 @@ const prerenderConfiguration = {
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
+ '/specialChars/search',
+ '/specialChars/malformed',
'/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 0f474c2b0df..e2aea1f9651 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..def40f2e94e 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'
@@ -45,7 +48,10 @@ import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/vi
import { Route as MultiCookieRedirectTargetRouteImport } from './routes/multi-cookie-redirect/target'
import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
+import { Route as SpecialCharsMalformedRouteRouteImport } from './routes/specialChars/malformed/route'
import { Route as RedirectTargetIndexRouteImport } from './routes/redirect/$target/index'
+import { Route as SpecialCharsMalformedSearchRouteImport } from './routes/specialChars/malformed/search'
+import { Route as SpecialCharsMalformedParamRouteImport } from './routes/specialChars/malformed/$param'
import { Route as RedirectTargetViaLoaderRouteImport } from './routes/redirect/$target/via-loader'
import { Route as RedirectTargetViaBeforeLoadRouteImport } from './routes/redirect/$target/via-beforeLoad'
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
@@ -57,12 +63,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 +107,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 +168,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',
@@ -239,11 +260,29 @@ const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
} as any)
+const SpecialCharsMalformedRouteRoute =
+ SpecialCharsMalformedRouteRouteImport.update({
+ id: '/malformed',
+ path: '/malformed',
+ getParentRoute: () => SpecialCharsRouteRoute,
+ } as any)
const RedirectTargetIndexRoute = RedirectTargetIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => RedirectTargetRoute,
} as any)
+const SpecialCharsMalformedSearchRoute =
+ SpecialCharsMalformedSearchRouteImport.update({
+ id: '/search',
+ path: '/search',
+ getParentRoute: () => SpecialCharsMalformedRouteRoute,
+ } as any)
+const SpecialCharsMalformedParamRoute =
+ SpecialCharsMalformedParamRouteImport.update({
+ id: '/$param',
+ path: '/$param',
+ getParentRoute: () => SpecialCharsMalformedRouteRoute,
+ } as any)
const RedirectTargetViaLoaderRoute = RedirectTargetViaLoaderRouteImport.update({
id: '/via-loader',
path: '/via-loader',
@@ -304,6 +343,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 +352,7 @@ export interface FileRoutesByFullPath {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -327,12 +367,15 @@ 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
+ '/multi-cookie-redirect/': typeof MultiCookieRedirectIndexRoute
'/not-found/': typeof NotFoundIndexRoute
'/posts/': typeof PostsIndexRoute
'/raw-stream/': typeof RawStreamIndexRoute
- '/redirect': typeof RedirectIndexRoute
+ '/redirect/': typeof RedirectIndexRoute
'/search-params/': typeof SearchParamsIndexRoute
'/users/': typeof UsersIndexRoute
'/layout-a': typeof LayoutLayout2LayoutARoute
@@ -341,20 +384,23 @@ export interface FileRoutesByFullPath {
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
'/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
'/redirect/$target/serverFn/via-useServerFn': typeof RedirectTargetServerFnViaUseServerFnRoute
- '/redirect/$target/serverFn': typeof RedirectTargetServerFnIndexRoute
+ '/redirect/$target/serverFn/': typeof RedirectTargetServerFnIndexRoute
}
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
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
@@ -368,6 +414,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
@@ -382,6 +431,8 @@ export interface FileRoutesByTo {
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/redirect/$target': typeof RedirectTargetIndexRoute
'/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
'/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
@@ -393,6 +444,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 +454,7 @@ export interface FileRoutesById {
'/scripts': typeof ScriptsRoute
'/stream': typeof StreamRoute
'/users': typeof UsersRouteWithChildren
- '/대한민국': typeof Char45824Char54620Char48124Char44397Route
+ '/specialChars/malformed': typeof SpecialCharsMalformedRouteRouteWithChildren
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/multi-cookie-redirect/target': typeof MultiCookieRedirectTargetRoute
@@ -418,6 +470,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
@@ -432,6 +487,8 @@ export interface FileRoutesById {
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
'/redirect/$target/via-beforeLoad': typeof RedirectTargetViaBeforeLoadRoute
'/redirect/$target/via-loader': typeof RedirectTargetViaLoaderRoute
+ '/specialChars/malformed/$param': typeof SpecialCharsMalformedParamRoute
+ '/specialChars/malformed/search': typeof SpecialCharsMalformedSearchRoute
'/redirect/$target/': typeof RedirectTargetIndexRoute
'/redirect/$target/serverFn/via-beforeLoad': typeof RedirectTargetServerFnViaBeforeLoadRoute
'/redirect/$target/serverFn/via-loader': typeof RedirectTargetServerFnViaLoaderRoute
@@ -444,6 +501,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
@@ -452,7 +510,7 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -467,12 +525,15 @@ export interface FileRouteTypes {
| '/redirect/$target'
| '/search-params/default'
| '/search-params/loader-throws-redirect'
+ | '/specialChars/$param'
+ | '/specialChars/search'
+ | '/specialChars/대한민국'
| '/users/$userId'
- | '/multi-cookie-redirect'
+ | '/multi-cookie-redirect/'
| '/not-found/'
| '/posts/'
| '/raw-stream/'
- | '/redirect'
+ | '/redirect/'
| '/search-params/'
| '/users/'
| '/layout-a'
@@ -481,20 +542,23 @@ export interface FileRouteTypes {
| '/posts/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/redirect/$target/'
| '/redirect/$target/serverFn/via-beforeLoad'
| '/redirect/$target/serverFn/via-loader'
| '/redirect/$target/serverFn/via-useServerFn'
- | '/redirect/$target/serverFn'
+ | '/redirect/$target/serverFn/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/specialChars'
| '/deferred'
| '/inline-scripts'
| '/links'
| '/scripts'
| '/stream'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/api/users'
| '/multi-cookie-redirect/target'
| '/not-found/via-beforeLoad'
@@ -508,6 +572,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'
@@ -522,6 +589,8 @@ export interface FileRouteTypes {
| '/posts/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/redirect/$target'
| '/redirect/$target/serverFn/via-beforeLoad'
| '/redirect/$target/serverFn/via-loader'
@@ -532,6 +601,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
+ | '/specialChars'
| '/_layout'
| '/deferred'
| '/inline-scripts'
@@ -541,7 +611,7 @@ export interface FileRouteTypes {
| '/scripts'
| '/stream'
| '/users'
- | '/대한민국'
+ | '/specialChars/malformed'
| '/_layout/_layout-2'
| '/api/users'
| '/multi-cookie-redirect/target'
@@ -557,6 +627,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/'
@@ -571,6 +644,8 @@ export interface FileRouteTypes {
| '/posts_/$postId/deep'
| '/redirect/$target/via-beforeLoad'
| '/redirect/$target/via-loader'
+ | '/specialChars/malformed/$param'
+ | '/specialChars/malformed/search'
| '/redirect/$target/'
| '/redirect/$target/serverFn/via-beforeLoad'
| '/redirect/$target/serverFn/via-loader'
@@ -582,6 +657,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 +667,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 +677,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'
@@ -668,10 +736,17 @@ declare module '@tanstack/vue-router' {
'/_layout': {
id: '/_layout'
path: ''
- fullPath: ''
+ fullPath: '/'
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'
@@ -710,7 +785,7 @@ declare module '@tanstack/vue-router' {
'/redirect/': {
id: '/redirect/'
path: '/redirect'
- fullPath: '/redirect'
+ fullPath: '/redirect/'
preLoaderRoute: typeof RedirectIndexRouteImport
parentRoute: typeof rootRouteImport
}
@@ -738,7 +813,7 @@ declare module '@tanstack/vue-router' {
'/multi-cookie-redirect/': {
id: '/multi-cookie-redirect/'
path: '/multi-cookie-redirect'
- fullPath: '/multi-cookie-redirect'
+ fullPath: '/multi-cookie-redirect/'
preLoaderRoute: typeof MultiCookieRedirectIndexRouteImport
parentRoute: typeof rootRouteImport
}
@@ -749,6 +824,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'
@@ -850,10 +946,17 @@ declare module '@tanstack/vue-router' {
'/_layout/_layout-2': {
id: '/_layout/_layout-2'
path: ''
- fullPath: ''
+ fullPath: '/'
preLoaderRoute: typeof LayoutLayout2RouteImport
parentRoute: typeof LayoutRoute
}
+ '/specialChars/malformed': {
+ id: '/specialChars/malformed'
+ path: '/malformed'
+ fullPath: '/specialChars/malformed'
+ preLoaderRoute: typeof SpecialCharsMalformedRouteRouteImport
+ parentRoute: typeof SpecialCharsRouteRoute
+ }
'/redirect/$target/': {
id: '/redirect/$target/'
path: '/'
@@ -861,6 +964,20 @@ declare module '@tanstack/vue-router' {
preLoaderRoute: typeof RedirectTargetIndexRouteImport
parentRoute: typeof RedirectTargetRoute
}
+ '/specialChars/malformed/search': {
+ id: '/specialChars/malformed/search'
+ path: '/search'
+ fullPath: '/specialChars/malformed/search'
+ preLoaderRoute: typeof SpecialCharsMalformedSearchRouteImport
+ parentRoute: typeof SpecialCharsMalformedRouteRoute
+ }
+ '/specialChars/malformed/$param': {
+ id: '/specialChars/malformed/$param'
+ path: '/$param'
+ fullPath: '/specialChars/malformed/$param'
+ preLoaderRoute: typeof SpecialCharsMalformedParamRouteImport
+ parentRoute: typeof SpecialCharsMalformedRouteRoute
+ }
'/redirect/$target/via-loader': {
id: '/redirect/$target/via-loader'
path: '/via-loader'
@@ -906,7 +1023,7 @@ declare module '@tanstack/vue-router' {
'/redirect/$target/serverFn/': {
id: '/redirect/$target/serverFn/'
path: '/serverFn'
- fullPath: '/redirect/$target/serverFn'
+ fullPath: '/redirect/$target/serverFn/'
preLoaderRoute: typeof RedirectTargetServerFnIndexRouteImport
parentRoute: typeof RedirectTargetRoute
}
@@ -965,6 +1082,40 @@ const SearchParamsRouteRouteChildren: SearchParamsRouteRouteChildren = {
const SearchParamsRouteRouteWithChildren =
SearchParamsRouteRoute._addFileChildren(SearchParamsRouteRouteChildren)
+interface SpecialCharsMalformedRouteRouteChildren {
+ SpecialCharsMalformedParamRoute: typeof SpecialCharsMalformedParamRoute
+ SpecialCharsMalformedSearchRoute: typeof SpecialCharsMalformedSearchRoute
+}
+
+const SpecialCharsMalformedRouteRouteChildren: SpecialCharsMalformedRouteRouteChildren =
+ {
+ SpecialCharsMalformedParamRoute: SpecialCharsMalformedParamRoute,
+ SpecialCharsMalformedSearchRoute: SpecialCharsMalformedSearchRoute,
+ }
+
+const SpecialCharsMalformedRouteRouteWithChildren =
+ SpecialCharsMalformedRouteRoute._addFileChildren(
+ SpecialCharsMalformedRouteRouteChildren,
+ )
+
+interface SpecialCharsRouteRouteChildren {
+ SpecialCharsMalformedRouteRoute: typeof SpecialCharsMalformedRouteRouteWithChildren
+ SpecialCharsParamRoute: typeof SpecialCharsParamRoute
+ SpecialCharsSearchRoute: typeof SpecialCharsSearchRoute
+ SpecialCharsChar45824Char54620Char48124Char44397Route: typeof SpecialCharsChar45824Char54620Char48124Char44397Route
+}
+
+const SpecialCharsRouteRouteChildren: SpecialCharsRouteRouteChildren = {
+ SpecialCharsMalformedRouteRoute: SpecialCharsMalformedRouteRouteWithChildren,
+ SpecialCharsParamRoute: SpecialCharsParamRoute,
+ SpecialCharsSearchRoute: SpecialCharsSearchRoute,
+ SpecialCharsChar45824Char54620Char48124Char44397Route:
+ SpecialCharsChar45824Char54620Char48124Char44397Route,
+}
+
+const SpecialCharsRouteRouteWithChildren =
+ SpecialCharsRouteRoute._addFileChildren(SpecialCharsRouteRouteChildren)
+
interface LayoutLayout2RouteChildren {
LayoutLayout2LayoutARoute: typeof LayoutLayout2LayoutARoute
LayoutLayout2LayoutBRoute: typeof LayoutLayout2LayoutBRoute
@@ -1080,6 +1231,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
+ SpecialCharsRouteRoute: SpecialCharsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
DeferredRoute: DeferredRoute,
InlineScriptsRoute: InlineScriptsRoute,
@@ -1089,8 +1241,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/malformed/$param.tsx b/e2e/vue-start/basic/src/routes/specialChars/malformed/$param.tsx
new file mode 100644
index 00000000000..71609645499
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/malformed/$param.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/specialChars/malformed/$param')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+
+ Hello "/specialChars/malformed/$param":{' '}
+ {params.value.param}
+
+ )
+}
diff --git a/e2e/vue-start/basic/src/routes/specialChars/malformed/route.tsx b/e2e/vue-start/basic/src/routes/specialChars/malformed/route.tsx
new file mode 100644
index 00000000000..1121505377b
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/malformed/route.tsx
@@ -0,0 +1,29 @@
+import { Link, Outlet, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/specialChars/malformed')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Hello "/specialChars/malformed"!
+
+ malformed path param
+ {' '}
+
+ malformed search param
+ {' '}
+
+
+
+ )
+}
diff --git a/e2e/vue-start/basic/src/routes/specialChars/malformed/search.tsx b/e2e/vue-start/basic/src/routes/specialChars/malformed/search.tsx
new file mode 100644
index 00000000000..24386049543
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/malformed/search.tsx
@@ -0,0 +1,22 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import z from 'zod'
+
+export const Route = createFileRoute('/specialChars/malformed/search')({
+ validateSearch: z.object({
+ searchParam: z.string(),
+ }),
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+
+ return (
+
+ Hello "/specialChars/malformed/search"!
+
+ {search.value.searchParam}
+
+
+ )
+}
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..a34cf5ee64a
--- /dev/null
+++ b/e2e/vue-start/basic/src/routes/specialChars/route.tsx
@@ -0,0 +1,53 @@
+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
+ {' '}
+
+ Malformed paths
+ {' '}
+
+
+
+ )
+}
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..8ff7e7762e6
--- /dev/null
+++ b/e2e/vue-start/basic/tests/special-characters.spec.ts
@@ -0,0 +1,171 @@
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import { isSpaMode } from './utils/isSpaMode'
+
+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('대|')
+ })
+ })
+
+ test.describe('malformed paths', () => {
+ test.use({
+ whitelistErrors: [
+ 'Failed to load resource: the server responded with a status of 404',
+ 'Failed to load resource: the server responded with a status of 400 (Bad Request)',
+ ],
+ })
+
+ test('un-matched malformed paths should return not found on direct navigation', async ({
+ page,
+ }) => {
+ const res = await page.goto('/specialChars/malformed/%E0%A4')
+
+ await page.waitForLoadState(`load`)
+
+ // in spa mode this is caught and handled at server level
+ if (!isSpaMode) {
+ expect(res!.status()).toBe(404)
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ } else {
+ expect(res!.status()).toBe(400)
+ }
+ })
+
+ test('malformed path params should return not found on router link', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/malformed')
+ await page.waitForURL(`${baseURL}/specialChars/malformed`)
+
+ const link = page.getByTestId('special-malformed-path-link')
+
+ await link.click()
+ await page.waitForLoadState('load')
+
+ await expect(
+ page.getByTestId('default-not-found-component'),
+ ).toBeInViewport()
+ })
+
+ test('un-matched malformed paths should return not found on direct navigation in search params', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/specialChars/malformed/search?searchParam=%E0%A4')
+
+ await page.waitForURL(
+ `${baseURL}/specialChars/malformed/search?searchParam=%E0%A4`,
+ )
+
+ await expect(
+ page.getByTestId('special-malformed-search-param'),
+ ).toBeInViewport()
+
+ const searchParam = await page
+ .getByTestId('special-malformed-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..222fce19b32 100644
--- a/e2e/vue-start/basic/vite.config.ts
+++ b/e2e/vue-start/basic/vite.config.ts
@@ -22,6 +22,8 @@ const prerenderConfiguration = {
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
+ '/specialChars/search',
+ '/specialChars/malformed',
'/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/new-process-route-tree.ts b/packages/router-core/src/new-process-route-tree.ts
index 2eef6426404..ef5387d2c99 100644
--- a/packages/router-core/src/new-process-route-tree.ts
+++ b/packages/router-core/src/new-process-route-tree.ts
@@ -732,11 +732,22 @@ export function findRouteMatch<
const cached = processedTree.matchCache.get(key)
if (cached !== undefined) return cached
path ||= '/'
- const result = findMatch(
- path,
- processedTree.segmentTree,
- fuzzy,
- ) as RouteMatch | null
+ let result: RouteMatch | null
+
+ try {
+ result = findMatch(
+ path,
+ processedTree.segmentTree,
+ fuzzy,
+ ) as RouteMatch | null
+ } catch (err) {
+ if (err instanceof URIError) {
+ result = null
+ } else {
+ throw err
+ }
+ }
+
if (result) result.branch = buildRouteBranch(result.route)
processedTree.matchCache.set(key, result)
return result
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..e8d05c4841b 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,25 @@ 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) {
+ // ensure backslashes are encoded correctly in the URL
+ if (typeof url === 'string') url = url.replace('\\', '%5C')
+
+ 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..6af6b2736a6
--- /dev/null
+++ b/packages/router-core/tests/getNormalizedURL.test.ts
@@ -0,0 +1,128 @@
+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',
+ },
+ {
+ url: 'https://example.com/%E0%A4',
+ expectedPathName: '/%E0%A4',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/%ZZ',
+ expectedPathName: '/%ZZ',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path?a=1&a=2',
+ expectedPathName: '/path',
+ expectedSearchParams: '?a=1&a=2',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path+a',
+ expectedPathName: '/path+a',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path a',
+ expectedPathName: '/path%20a',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path%20a',
+ expectedPathName: '/path%20a',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path%25a',
+ expectedPathName: '/path%25a',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path%25a',
+ expectedPathName: '/path%25a',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path\\a',
+ expectedPathName: '/path%5Ca',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ {
+ url: 'https://example.com/path%5Ca',
+ expectedPathName: '/path%5Ca',
+ expectedSearchParams: '',
+ expectedHash: '',
+ },
+ ]
+ 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..7fa0e4a4843 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,8 +217,9 @@ export function createStartHandler(
let cbWillCleanup = false as boolean
try {
- const url = new URL(request.url)
- const href = url.href.replace(url.origin, '')
+ // 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.pathname + url.search + url.hash
const origin = getOrigin(request)
const entries = await getEntries()