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()