From 3954f7c6a290ba4f0c0275a857ad01bf0fcb3806 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 19:24:52 +0100 Subject: [PATCH 1/6] vue-start server-routes --- e2e/vue-start/server-routes/.gitignore | 20 ++ e2e/vue-start/server-routes/.prettierignore | 4 + e2e/vue-start/server-routes/package.json | 42 +++ .../server-routes/playwright.config.ts | 35 +++ .../server-routes/postcss.config.mjs | 5 + .../server-routes/public/favicon.ico | Bin 0 -> 15406 bytes .../server-routes/public/favicon.png | Bin 0 -> 1507 bytes .../src/components/DefaultCatchBoundary.tsx | 53 ++++ .../server-routes/src/components/NotFound.tsx | 25 ++ .../server-routes/src/routeTree.gen.ts | 257 ++++++++++++++++++ e2e/vue-start/server-routes/src/router.tsx | 20 ++ .../server-routes/src/routes/__root.tsx | 45 +++ .../src/routes/api/middleware-context.ts | 29 ++ .../server-routes/src/routes/api/only-any.ts | 18 ++ .../src/routes/api/params/$foo/$bar.ts | 11 + .../src/routes/api/params/$foo/route.ts | 11 + .../server-routes/src/routes/index.tsx | 23 ++ .../src/routes/merge-middleware-context.tsx | 68 +++++ .../src/routes/methods/index.tsx | 19 ++ .../src/routes/methods/only-any.tsx | 64 +++++ .../src/routes/methods/route.tsx | 14 + .../server-routes/src/styles/app.css | 30 ++ e2e/vue-start/server-routes/src/vite-env.d.ts | 4 + .../server-routes/tests/server-routes.spec.ts | 47 ++++ e2e/vue-start/server-routes/tsconfig.json | 23 ++ e2e/vue-start/server-routes/vite.config.ts | 19 ++ pnpm-lock.yaml | 76 ++++++ 27 files changed, 962 insertions(+) create mode 100644 e2e/vue-start/server-routes/.gitignore create mode 100644 e2e/vue-start/server-routes/.prettierignore create mode 100644 e2e/vue-start/server-routes/package.json create mode 100644 e2e/vue-start/server-routes/playwright.config.ts create mode 100644 e2e/vue-start/server-routes/postcss.config.mjs create mode 100644 e2e/vue-start/server-routes/public/favicon.ico create mode 100644 e2e/vue-start/server-routes/public/favicon.png create mode 100644 e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx create mode 100644 e2e/vue-start/server-routes/src/components/NotFound.tsx create mode 100644 e2e/vue-start/server-routes/src/routeTree.gen.ts create mode 100644 e2e/vue-start/server-routes/src/router.tsx create mode 100644 e2e/vue-start/server-routes/src/routes/__root.tsx create mode 100644 e2e/vue-start/server-routes/src/routes/api/middleware-context.ts create mode 100644 e2e/vue-start/server-routes/src/routes/api/only-any.ts create mode 100644 e2e/vue-start/server-routes/src/routes/api/params/$foo/$bar.ts create mode 100644 e2e/vue-start/server-routes/src/routes/api/params/$foo/route.ts create mode 100644 e2e/vue-start/server-routes/src/routes/index.tsx create mode 100644 e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx create mode 100644 e2e/vue-start/server-routes/src/routes/methods/index.tsx create mode 100644 e2e/vue-start/server-routes/src/routes/methods/only-any.tsx create mode 100644 e2e/vue-start/server-routes/src/routes/methods/route.tsx create mode 100644 e2e/vue-start/server-routes/src/styles/app.css create mode 100644 e2e/vue-start/server-routes/src/vite-env.d.ts create mode 100644 e2e/vue-start/server-routes/tests/server-routes.spec.ts create mode 100644 e2e/vue-start/server-routes/tsconfig.json create mode 100644 e2e/vue-start/server-routes/vite.config.ts diff --git a/e2e/vue-start/server-routes/.gitignore b/e2e/vue-start/server-routes/.gitignore new file mode 100644 index 00000000000..a79d5cf1299 --- /dev/null +++ b/e2e/vue-start/server-routes/.gitignore @@ -0,0 +1,20 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output + +/build/ +/api/ +/server/build +/public/build +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/vue-start/server-routes/.prettierignore b/e2e/vue-start/server-routes/.prettierignore new file mode 100644 index 00000000000..2be5eaa6ece --- /dev/null +++ b/e2e/vue-start/server-routes/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/vue-start/server-routes/package.json b/e2e/vue-start/server-routes/package.json new file mode 100644 index 00000000000..31a1444bccb --- /dev/null +++ b/e2e/vue-start/server-routes/package.json @@ -0,0 +1,42 @@ +{ + "name": "tanstack-vue-start-e2e-server-routes", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "preview": "vite preview", + "start": "pnpx srvx --prod -s ../client dist/server/server.js", + "test:e2e": "playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/vue-query": "^5.90.9", + "@tanstack/vue-router": "workspace:^", + "@tanstack/vue-router-devtools": "workspace:^", + "@tanstack/vue-router-ssr-query": "workspace:^", + "@tanstack/vue-start": "workspace:^", + "js-cookie": "^3.0.5", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vite": "^7.1.7", + "vue": "^3.5.25", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.10.2", + "combinate": "^1.1.11", + "postcss": "^8.5.1", + "srvx": "^0.8.6", + "tailwindcss": "^4.1.17", + "typescript": "^5.7.2", + "@vitejs/plugin-vue": "^6.0.3", + "@vitejs/plugin-vue-jsx": "^5.1.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/vue-start/server-routes/playwright.config.ts b/e2e/vue-start/server-routes/playwright.config.ts new file mode 100644 index 00000000000..cb1da03b942 --- /dev/null +++ b/e2e/vue-start/server-routes/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +export const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `pnpm build && VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/vue-start/server-routes/postcss.config.mjs b/e2e/vue-start/server-routes/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/vue-start/server-routes/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/vue-start/server-routes/public/favicon.ico b/e2e/vue-start/server-routes/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a1751676f7e22811b1070572093996c93c87617 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/e2e/vue-start/server-routes/public/favicon.png b/e2e/vue-start/server-routes/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1e77bc06091ade4496525a09d8900675afcf03f0 GIT binary patch literal 1507 zcmV<91swW`P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^O-V#SR9Fd>S3Qp$MHK9ro!#4$ zEP@L_hX|b@f=!*_42h6mKu7{)4)_U*>1>0bCkUj;Z1X!7 zHe(Ew^Oi(|bW3J~xu+)XbtFF?4>!7TH$>(D_atUQVEj(8fGvYu2NF33#JZX>)(Vj8 zIi@z>Glt?6t~;Lf(|C8F>;WF^8F<^s7Scr!sZc01uB?HMHoL5+FZ>B(g+r-)?Sn)#3Zal#?G@GAwO5U27MpGOlC2+_saA)rl zP-<@-n~;PQOlm|Hi<+W;NdR;5+=zADzM&?!+CPD36=cGwHy6!D^vPEHG?rO`K>G|M z3FposX{yT132wuw1OR3Um_5JoKB#6?!QgBupIT;?YIr;WcpmuCE>S75mZid+ens#E zGPuYjiG0UNNVWu=f!Id^?9)34)eIpu-`j_~W0iAQzK(}XYc_!;87Tk~?4tq|h=2(! zuq0HCiNK)@+ocCKR3q1REdUju>HdYxd>JX@%oOibg+J~D+}rhz54D!NfC{h-OYk{M zkzmFtdrL@nL0bm8nF@pob1CeLC>12ef#in-Bzv2!wi)Iuwq24)`AH}|0QNQ^f$KHv z?5PBPo1*#GAuAk+Poe`?UJ>mP`@~d4a(103j0lwUx@_+$#B&VC%7r>#2$HIiD`KO8L|s3Yp%M}BT0;NJDzZtPnx=4%enhU zhW*pNN0t`^4%5MKAR+}=^Q?QeqQ`>bbK zf+-ji$Uz8V0?LpX@kh`k%DL)GCA2=@SJNKg56Wh>>pr=7{1PmHqG|~=AdLV3002ov JPDHLkV1ivgp)>#h literal 0 HcmV?d00001 diff --git a/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx b/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..d6b3c732885 --- /dev/null +++ b/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/vue-router' +import type { ErrorComponentProps } from '@tanstack/vue-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot() ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/vue-start/server-routes/src/components/NotFound.tsx b/e2e/vue-start/server-routes/src/components/NotFound.tsx new file mode 100644 index 00000000000..944e35c12c6 --- /dev/null +++ b/e2e/vue-start/server-routes/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/vue-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/vue-start/server-routes/src/routeTree.gen.ts b/e2e/vue-start/server-routes/src/routeTree.gen.ts new file mode 100644 index 00000000000..7d9d463772e --- /dev/null +++ b/e2e/vue-start/server-routes/src/routeTree.gen.ts @@ -0,0 +1,257 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// 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 MergeMiddlewareContextRouteImport } from './routes/merge-middleware-context' +import { Route as MethodsRouteRouteImport } from './routes/methods/route' +import { Route as IndexRouteImport } from './routes/index' +import { Route as MethodsIndexRouteImport } from './routes/methods/index' +import { Route as MethodsOnlyAnyRouteImport } from './routes/methods/only-any' +import { Route as ApiOnlyAnyRouteImport } from './routes/api/only-any' +import { Route as ApiMiddlewareContextRouteImport } from './routes/api/middleware-context' +import { Route as ApiParamsFooRouteRouteImport } from './routes/api/params/$foo/route' +import { Route as ApiParamsFooBarRouteImport } from './routes/api/params/$foo/$bar' + +const MergeMiddlewareContextRoute = MergeMiddlewareContextRouteImport.update({ + id: '/merge-middleware-context', + path: '/merge-middleware-context', + getParentRoute: () => rootRouteImport, +} as any) +const MethodsRouteRoute = MethodsRouteRouteImport.update({ + id: '/methods', + path: '/methods', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const MethodsIndexRoute = MethodsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => MethodsRouteRoute, +} as any) +const MethodsOnlyAnyRoute = MethodsOnlyAnyRouteImport.update({ + id: '/only-any', + path: '/only-any', + getParentRoute: () => MethodsRouteRoute, +} as any) +const ApiOnlyAnyRoute = ApiOnlyAnyRouteImport.update({ + id: '/api/only-any', + path: '/api/only-any', + getParentRoute: () => rootRouteImport, +} as any) +const ApiMiddlewareContextRoute = ApiMiddlewareContextRouteImport.update({ + id: '/api/middleware-context', + path: '/api/middleware-context', + getParentRoute: () => rootRouteImport, +} as any) +const ApiParamsFooRouteRoute = ApiParamsFooRouteRouteImport.update({ + id: '/api/params/$foo', + path: '/api/params/$foo', + getParentRoute: () => rootRouteImport, +} as any) +const ApiParamsFooBarRoute = ApiParamsFooBarRouteImport.update({ + id: '/$bar', + path: '/$bar', + getParentRoute: () => ApiParamsFooRouteRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/methods': typeof MethodsRouteRouteWithChildren + '/merge-middleware-context': typeof MergeMiddlewareContextRoute + '/api/middleware-context': typeof ApiMiddlewareContextRoute + '/api/only-any': typeof ApiOnlyAnyRoute + '/methods/only-any': typeof MethodsOnlyAnyRoute + '/methods/': typeof MethodsIndexRoute + '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren + '/api/params/$foo/$bar': typeof ApiParamsFooBarRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/merge-middleware-context': typeof MergeMiddlewareContextRoute + '/api/middleware-context': typeof ApiMiddlewareContextRoute + '/api/only-any': typeof ApiOnlyAnyRoute + '/methods/only-any': typeof MethodsOnlyAnyRoute + '/methods': typeof MethodsIndexRoute + '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren + '/api/params/$foo/$bar': typeof ApiParamsFooBarRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/methods': typeof MethodsRouteRouteWithChildren + '/merge-middleware-context': typeof MergeMiddlewareContextRoute + '/api/middleware-context': typeof ApiMiddlewareContextRoute + '/api/only-any': typeof ApiOnlyAnyRoute + '/methods/only-any': typeof MethodsOnlyAnyRoute + '/methods/': typeof MethodsIndexRoute + '/api/params/$foo': typeof ApiParamsFooRouteRouteWithChildren + '/api/params/$foo/$bar': typeof ApiParamsFooBarRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/methods' + | '/merge-middleware-context' + | '/api/middleware-context' + | '/api/only-any' + | '/methods/only-any' + | '/methods/' + | '/api/params/$foo' + | '/api/params/$foo/$bar' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/merge-middleware-context' + | '/api/middleware-context' + | '/api/only-any' + | '/methods/only-any' + | '/methods' + | '/api/params/$foo' + | '/api/params/$foo/$bar' + id: + | '__root__' + | '/' + | '/methods' + | '/merge-middleware-context' + | '/api/middleware-context' + | '/api/only-any' + | '/methods/only-any' + | '/methods/' + | '/api/params/$foo' + | '/api/params/$foo/$bar' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + MethodsRouteRoute: typeof MethodsRouteRouteWithChildren + MergeMiddlewareContextRoute: typeof MergeMiddlewareContextRoute + ApiMiddlewareContextRoute: typeof ApiMiddlewareContextRoute + ApiOnlyAnyRoute: typeof ApiOnlyAnyRoute + ApiParamsFooRouteRoute: typeof ApiParamsFooRouteRouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/merge-middleware-context': { + id: '/merge-middleware-context' + path: '/merge-middleware-context' + fullPath: '/merge-middleware-context' + preLoaderRoute: typeof MergeMiddlewareContextRouteImport + parentRoute: typeof rootRouteImport + } + '/methods': { + id: '/methods' + path: '/methods' + fullPath: '/methods' + preLoaderRoute: typeof MethodsRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/methods/': { + id: '/methods/' + path: '/' + fullPath: '/methods/' + preLoaderRoute: typeof MethodsIndexRouteImport + parentRoute: typeof MethodsRouteRoute + } + '/methods/only-any': { + id: '/methods/only-any' + path: '/only-any' + fullPath: '/methods/only-any' + preLoaderRoute: typeof MethodsOnlyAnyRouteImport + parentRoute: typeof MethodsRouteRoute + } + '/api/only-any': { + id: '/api/only-any' + path: '/api/only-any' + fullPath: '/api/only-any' + preLoaderRoute: typeof ApiOnlyAnyRouteImport + parentRoute: typeof rootRouteImport + } + '/api/middleware-context': { + id: '/api/middleware-context' + path: '/api/middleware-context' + fullPath: '/api/middleware-context' + preLoaderRoute: typeof ApiMiddlewareContextRouteImport + parentRoute: typeof rootRouteImport + } + '/api/params/$foo': { + id: '/api/params/$foo' + path: '/api/params/$foo' + fullPath: '/api/params/$foo' + preLoaderRoute: typeof ApiParamsFooRouteRouteImport + parentRoute: typeof rootRouteImport + } + '/api/params/$foo/$bar': { + id: '/api/params/$foo/$bar' + path: '/$bar' + fullPath: '/api/params/$foo/$bar' + preLoaderRoute: typeof ApiParamsFooBarRouteImport + parentRoute: typeof ApiParamsFooRouteRoute + } + } +} + +interface MethodsRouteRouteChildren { + MethodsOnlyAnyRoute: typeof MethodsOnlyAnyRoute + MethodsIndexRoute: typeof MethodsIndexRoute +} + +const MethodsRouteRouteChildren: MethodsRouteRouteChildren = { + MethodsOnlyAnyRoute: MethodsOnlyAnyRoute, + MethodsIndexRoute: MethodsIndexRoute, +} + +const MethodsRouteRouteWithChildren = MethodsRouteRoute._addFileChildren( + MethodsRouteRouteChildren, +) + +interface ApiParamsFooRouteRouteChildren { + ApiParamsFooBarRoute: typeof ApiParamsFooBarRoute +} + +const ApiParamsFooRouteRouteChildren: ApiParamsFooRouteRouteChildren = { + ApiParamsFooBarRoute: ApiParamsFooBarRoute, +} + +const ApiParamsFooRouteRouteWithChildren = + ApiParamsFooRouteRoute._addFileChildren(ApiParamsFooRouteRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + MethodsRouteRoute: MethodsRouteRouteWithChildren, + MergeMiddlewareContextRoute: MergeMiddlewareContextRoute, + ApiMiddlewareContextRoute: ApiMiddlewareContextRoute, + ApiOnlyAnyRoute: ApiOnlyAnyRoute, + ApiParamsFooRouteRoute: ApiParamsFooRouteRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/vue-start/server-routes/src/router.tsx b/e2e/vue-start/server-routes/src/router.tsx new file mode 100644 index 00000000000..b6f983861e0 --- /dev/null +++ b/e2e/vue-start/server-routes/src/router.tsx @@ -0,0 +1,20 @@ +import { createRouter } from '@tanstack/vue-router' +import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query' +import { QueryClient } from '@tanstack/solid-query' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function getRouter() { + const queryClient = new QueryClient() + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + }) + setupRouterSsrQueryIntegration({ router, queryClient }) + + return router +} diff --git a/e2e/vue-start/server-routes/src/routes/__root.tsx b/e2e/vue-start/server-routes/src/routes/__root.tsx new file mode 100644 index 00000000000..c15c1fe146f --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/__root.tsx @@ -0,0 +1,45 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +import { TanStackRouterDevtools } from '@tanstack/vue-router-devtools' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ], + links: [{ rel: 'stylesheet', href: appCss }], + }), + errorComponent: (props) => { + return

{props.error.stack}

+ }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/e2e/vue-start/server-routes/src/routes/api/middleware-context.ts b/e2e/vue-start/server-routes/src/routes/api/middleware-context.ts new file mode 100644 index 00000000000..d780d2ebb7b --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/api/middleware-context.ts @@ -0,0 +1,29 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createMiddleware, json } from '@tanstack/vue-start' + +const testParentMiddleware = createMiddleware().server(async ({ next }) => { + const result = await next({ context: { testParent: true } }) + return result +}) + +const testMiddleware = createMiddleware() + .middleware([testParentMiddleware]) + .server(async ({ next }) => { + const result = await next({ context: { test: true } }) + return result + }) + +export const Route = createFileRoute('/api/middleware-context')({ + server: { + middleware: [testMiddleware], + handlers: { + GET: ({ request, context }) => { + return json({ + url: request.url, + context: context, + expectedContext: { testParent: true, test: true }, + }) + }, + }, + }, +}) diff --git a/e2e/vue-start/server-routes/src/routes/api/only-any.ts b/e2e/vue-start/server-routes/src/routes/api/only-any.ts new file mode 100644 index 00000000000..ded160acc18 --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/api/only-any.ts @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { json } from '@tanstack/vue-start' + +export const Route = createFileRoute('/api/only-any')({ + server: { + handlers: { + ANY: ({ request }) => { + return json( + { + handler: 'ANY', + method: request.method, + }, + { headers: { 'X-HANDLER': 'ANY', 'X-METHOD': request.method } }, + ) + }, + }, + }, +}) diff --git a/e2e/vue-start/server-routes/src/routes/api/params/$foo/$bar.ts b/e2e/vue-start/server-routes/src/routes/api/params/$foo/$bar.ts new file mode 100644 index 00000000000..3f11e8287d8 --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/api/params/$foo/$bar.ts @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/api/params/$foo/$bar')({ + server: { + handlers: { + GET: ({ params }) => { + return new Response('hello, ' + params.foo + ' and ' + params.bar) + }, + }, + }, +}) diff --git a/e2e/vue-start/server-routes/src/routes/api/params/$foo/route.ts b/e2e/vue-start/server-routes/src/routes/api/params/$foo/route.ts new file mode 100644 index 00000000000..771e53025e9 --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/api/params/$foo/route.ts @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/api/params/$foo')({ + server: { + handlers: { + GET: ({ params }) => { + return new Response('hello, ' + params.foo) + }, + }, + }, +}) diff --git a/e2e/vue-start/server-routes/src/routes/index.tsx b/e2e/vue-start/server-routes/src/routes/index.tsx new file mode 100644 index 00000000000..78d831d3250 --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/index.tsx @@ -0,0 +1,23 @@ +import { Link, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Server routes E2E tests

+
    +
  • + + server route middleware context is merged correctly + +
  • +
  • + server route methods +
  • +
+
+ ) +} diff --git a/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx b/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx new file mode 100644 index 00000000000..b50ce1dbb3e --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx @@ -0,0 +1,68 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createSignal } from 'solid-js' + +export const Route = createFileRoute('/merge-middleware-context')({ + component: () => , +}) + +function MergeMiddlewareContext() { + const [apiResponse, setApiResponse] = createSignal(null) + + const fetchMiddlewareContext = async () => { + try { + const response = await fetch('/api/middleware-context') + const data = await response.json() + setApiResponse(data) + } catch (error) { + console.error('Error fetching middleware context:', error) + setApiResponse({ error: 'Failed to fetch' }) + } + } + + return ( +
+

Merge Server Route Middleware Context Test

+
+ + + {apiResponse() && ( +
+

API Response:

+
+              {JSON.stringify(apiResponse(), null, 2)}
+            
+ +
+

Context Verification:

+
+ {JSON.stringify(apiResponse()?.context, null, 2)} +
+ +
+ Has testParent:{' '} + {apiResponse()?.context?.testParent ? 'true' : 'false'} +
+ +
+ Has test: {apiResponse()?.context?.test ? 'true' : 'false'} +
+
+
+ )} +
+
+ ) +} diff --git a/e2e/vue-start/server-routes/src/routes/methods/index.tsx b/e2e/vue-start/server-routes/src/routes/methods/index.tsx new file mode 100644 index 00000000000..a46a0e66ad0 --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/methods/index.tsx @@ -0,0 +1,19 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/methods/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+
    +
  • + + Server Route only has ANY handler + +
  • +
+
+ ) +} diff --git a/e2e/vue-start/server-routes/src/routes/methods/only-any.tsx b/e2e/vue-start/server-routes/src/routes/methods/only-any.tsx new file mode 100644 index 00000000000..4fa6467f778 --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/methods/only-any.tsx @@ -0,0 +1,64 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { useQuery } from '@tanstack/solid-query' +import { For } from 'solid-js' + +export const Route = createFileRoute('/methods/only-any')({ + ssr: false, + component: RouteComponent, +}) + +const HttpMethods = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + 'OPTIONS', + 'HEAD', +] as const +type HttpMethods = (typeof HttpMethods)[number] + +function Test(props: { method: HttpMethods }) { + const query = useQuery(() => ({ + queryKey: [props.method], + queryFn: async () => { + const response = await fetch(`/api/only-any`, { + method: props.method, + }) + try { + const json = (await response.json()) as Promise<{ + method: HttpMethods + handler: HttpMethods & 'ANY' + }> + return json + } catch (e) {} + // handle HEAD and OPTIONS that have no body + const result = { + handler: response.headers.get('x-handler') as HttpMethods & 'ANY', + method: response.headers.get('x-method') as HttpMethods, + } + return result + }, + })) + + return ( +
+

method={props.method}

+

expected

+
{props.method}
+

result

+ {query.data ? ( +
{query.data.method}
+ ) : null} +
+ ) +} + +function RouteComponent() { + return ( +
+

Server Route has only ANY handler

+ {(method) => } +
+ ) +} diff --git a/e2e/vue-start/server-routes/src/routes/methods/route.tsx b/e2e/vue-start/server-routes/src/routes/methods/route.tsx new file mode 100644 index 00000000000..e39dc52bd8d --- /dev/null +++ b/e2e/vue-start/server-routes/src/routes/methods/route.tsx @@ -0,0 +1,14 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/methods')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

Server Routes Methods E2E tests

+ +
+ ) +} diff --git a/e2e/vue-start/server-routes/src/styles/app.css b/e2e/vue-start/server-routes/src/styles/app.css new file mode 100644 index 00000000000..c36c737cd46 --- /dev/null +++ b/e2e/vue-start/server-routes/src/styles/app.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/vue-start/server-routes/src/vite-env.d.ts b/e2e/vue-start/server-routes/src/vite-env.d.ts new file mode 100644 index 00000000000..0b2af560d60 --- /dev/null +++ b/e2e/vue-start/server-routes/src/vite-env.d.ts @@ -0,0 +1,4 @@ +declare module '*?url' { + const url: string + export default url +} diff --git a/e2e/vue-start/server-routes/tests/server-routes.spec.ts b/e2e/vue-start/server-routes/tests/server-routes.spec.ts new file mode 100644 index 00000000000..f3a80a5a469 --- /dev/null +++ b/e2e/vue-start/server-routes/tests/server-routes.spec.ts @@ -0,0 +1,47 @@ +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' + +test('merge-middleware-context', async ({ page }) => { + await page.goto('/merge-middleware-context') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('test-middleware-context-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('has-test-parent')).toContainText('true') + await expect(page.getByTestId('has-test')).toContainText('true') + + const contextResult = await page.getByTestId('context-result').textContent() + expect(contextResult).toContain('testParent') + expect(contextResult).toContain('test') +}) + +test.describe('methods', () => { + test('only ANY', async ({ page }) => { + await page.goto('/methods/only-any') + + // wait for page to be loaded by waiting for the route component to be rendered + await expect(page.getByTestId('route-component')).toBeInViewport() + + const testCases = await page + .locator('[data-testid^="expected-"]') + .elementHandles() + expect(testCases.length).not.toBe(0) + for (const testCase of testCases) { + const testId = await testCase.getAttribute('data-testid') + + if (!testId) { + throw new Error('testcase is missing data-testid') + } + + const suffix = testId.replace('expected-', '') + + const expected = + (await page.getByTestId(`expected-${suffix}`).textContent()) || '' + expect(expected).not.toBe('') + + await expect(page.getByTestId(`result-${suffix}`)).toContainText(expected) + } + }) +}) diff --git a/e2e/vue-start/server-routes/tsconfig.json b/e2e/vue-start/server-routes/tsconfig.json new file mode 100644 index 00000000000..a5ae5ae7e47 --- /dev/null +++ b/e2e/vue-start/server-routes/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/vue-start/server-routes/vite.config.ts b/e2e/vue-start/server-routes/vite.config.ts new file mode 100644 index 00000000000..79edfc05c26 --- /dev/null +++ b/e2e/vue-start/server-routes/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart(), + vue(), + vueJsx(), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b41ca8d2f5..b6ff0805244 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4965,6 +4965,82 @@ importers: specifier: ^3.1.8 version: 3.1.8(typescript@5.9.2) + e2e/vue-start/server-routes: + dependencies: + '@tanstack/vue-query': + specifier: ^5.90.9 + version: 5.92.0(vue@3.5.25(typescript@5.9.2)) + '@tanstack/vue-router': + specifier: workspace:* + version: link:../../../packages/vue-router + '@tanstack/vue-router-devtools': + specifier: workspace:* + version: link:../../../packages/vue-router-devtools + '@tanstack/vue-router-ssr-query': + specifier: workspace:* + version: link:../../../packages/vue-router-ssr-query + '@tanstack/vue-start': + specifier: workspace:* + version: link:../../../packages/vue-start + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vue: + specifier: ^3.5.25 + version: 3.5.25(typescript@5.9.2) + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.15 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + '@vitejs/plugin-vue': + specifier: ^6.0.3 + version: 6.0.3(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) + '@vitejs/plugin-vue-jsx': + specifier: ^5.1.2 + version: 5.1.2(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + postcss: + specifier: ^8.5.1 + version: 8.5.6 + srvx: + specifier: ^0.8.6 + version: 0.8.15 + tailwindcss: + specifier: ^4.1.17 + version: 4.1.17 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + examples/react/authenticated-routes: dependencies: '@tailwindcss/postcss': From ba11b8a069838569b4975b8210fbf06c8517fdc2 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 19:29:02 +0100 Subject: [PATCH 2/6] use vue-query --- e2e/vue-start/server-routes/src/router.tsx | 2 +- .../src/routes/methods/only-any.tsx | 88 +++++++++++-------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/e2e/vue-start/server-routes/src/router.tsx b/e2e/vue-start/server-routes/src/router.tsx index b6f983861e0..683d7776dd8 100644 --- a/e2e/vue-start/server-routes/src/router.tsx +++ b/e2e/vue-start/server-routes/src/router.tsx @@ -1,6 +1,6 @@ import { createRouter } from '@tanstack/vue-router' import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query' -import { QueryClient } from '@tanstack/solid-query' +import { QueryClient } from '@tanstack/vue-query' import { routeTree } from './routeTree.gen' import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' import { NotFound } from './components/NotFound' diff --git a/e2e/vue-start/server-routes/src/routes/methods/only-any.tsx b/e2e/vue-start/server-routes/src/routes/methods/only-any.tsx index 4fa6467f778..7c468e5ee98 100644 --- a/e2e/vue-start/server-routes/src/routes/methods/only-any.tsx +++ b/e2e/vue-start/server-routes/src/routes/methods/only-any.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' -import { useQuery } from '@tanstack/solid-query' -import { For } from 'solid-js' +import { useQuery } from '@tanstack/vue-query' +import { defineComponent } from 'vue' export const Route = createFileRoute('/methods/only-any')({ ssr: false, @@ -18,47 +18,63 @@ const HttpMethods = [ ] as const type HttpMethods = (typeof HttpMethods)[number] -function Test(props: { method: HttpMethods }) { - const query = useQuery(() => ({ - queryKey: [props.method], - queryFn: async () => { - const response = await fetch(`/api/only-any`, { - method: props.method, - }) - try { - const json = (await response.json()) as Promise<{ - method: HttpMethods - handler: HttpMethods & 'ANY' - }> - return json - } catch (e) {} - // handle HEAD and OPTIONS that have no body - const result = { - handler: response.headers.get('x-handler') as HttpMethods & 'ANY', - method: response.headers.get('x-method') as HttpMethods, - } - return result +type OnlyAnyApiResponse = { + method: HttpMethods + handler: 'ANY' +} + +const Test = defineComponent({ + name: 'OnlyAnyTest', + props: { + method: { + type: String, + required: true, }, - })) + }, + setup(props) { + const query = useQuery(() => ({ + queryKey: ['only-any', props.method], + queryFn: async () => { + const method = props.method as HttpMethods + const response = await fetch(`/api/only-any`, { method }) - return ( -
-

method={props.method}

-

expected

-
{props.method}
-

result

- {query.data ? ( -
{query.data.method}
- ) : null} -
- ) -} + try { + return (await response.json()) as OnlyAnyApiResponse + } catch { + // handle HEAD and OPTIONS that have no body + return { + handler: (response.headers.get('x-handler') ?? + 'ANY') as OnlyAnyApiResponse['handler'], + method: (response.headers.get('x-method') ?? + method) as OnlyAnyApiResponse['method'], + } + } + }, + })) + + return () => ( +
+

method={props.method}

+

expected

+
{props.method}
+

result

+ {query.data.value ? ( +
+ {query.data.value.method} +
+ ) : null} +
+ ) + }, +}) function RouteComponent() { return (

Server Route has only ANY handler

- {(method) => } + {HttpMethods.map((method) => ( + + ))}
) } From 9104e00b8ca9a0372e8ee4bc123fe5c18cdc51d2 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 19:29:35 +0100 Subject: [PATCH 3/6] use html/body --- .../src/components/DefaultCatchBoundary.tsx | 2 +- e2e/vue-start/server-routes/src/routes/__root.tsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx b/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx index d6b3c732885..e11970b6007 100644 --- a/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx +++ b/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx @@ -28,7 +28,7 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) { > Try Again - {isRoot() ? ( + {isRoot.value ? ( + - + - - + + ) } From 2d8429e0bb81fdd61ce4b39d823c4167c70ef03c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 19:40:20 +0100 Subject: [PATCH 4/6] test(vue-router): server-routes --- .../src/components/DefaultCatchBoundary.tsx | 2 +- .../src/routes/merge-middleware-context.tsx | 116 +++++++++--------- packages/vue-router/src/index.tsx | 1 + packages/vue-router/src/link.tsx | 20 +++ packages/vue-router/src/route.ts | 30 ++++- 5 files changed, 112 insertions(+), 57 deletions(-) diff --git a/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx b/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx index e11970b6007..b1f818dd747 100644 --- a/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx +++ b/e2e/vue-start/server-routes/src/components/DefaultCatchBoundary.tsx @@ -39,7 +39,7 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) { { + onClick={(e: MouseEvent) => { e.preventDefault() window.history.back() }} diff --git a/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx b/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx index b50ce1dbb3e..76eac749ac7 100644 --- a/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx +++ b/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx @@ -1,68 +1,74 @@ import { createFileRoute } from '@tanstack/vue-router' -import { createSignal } from 'solid-js' +import { defineComponent, ref } from 'vue' -export const Route = createFileRoute('/merge-middleware-context')({ - component: () => , -}) - -function MergeMiddlewareContext() { - const [apiResponse, setApiResponse] = createSignal(null) +const MergeMiddlewareContext = defineComponent({ + name: 'MergeMiddlewareContext', + setup() { + const apiResponse = ref(null) - const fetchMiddlewareContext = async () => { - try { - const response = await fetch('/api/middleware-context') - const data = await response.json() - setApiResponse(data) - } catch (error) { - console.error('Error fetching middleware context:', error) - setApiResponse({ error: 'Failed to fetch' }) + const fetchMiddlewareContext = async () => { + try { + const response = await fetch('/api/middleware-context') + const data = await response.json() + apiResponse.value = data + } catch (error) { + console.error('Error fetching middleware context:', error) + apiResponse.value = { error: 'Failed to fetch' } + } } - } - return ( -
-

Merge Server Route Middleware Context Test

-
- + return () => ( +
+

Merge Server Route Middleware Context Test

+
+ - {apiResponse() && ( -
-

API Response:

-
-              {JSON.stringify(apiResponse(), null, 2)}
-            
- -
-

Context Verification:

-
+

API Response:

+
-                {JSON.stringify(apiResponse()?.context, null, 2)}
-              
+ {JSON.stringify(apiResponse.value, null, 2)} + -
- Has testParent:{' '} - {apiResponse()?.context?.testParent ? 'true' : 'false'} -
+
+

Context Verification:

+
+ {JSON.stringify(apiResponse.value?.context, null, 2)} +
-
- Has test: {apiResponse()?.context?.test ? 'true' : 'false'} +
+ Has testParent:{' '} + {apiResponse.value?.context?.testParent ? 'true' : 'false'} +
+ +
+ Has test: {apiResponse.value?.context?.test ? 'true' : 'false'} +
-
- )} + ) : null} +
-
- ) -} + ) + }, +}) + +export const Route = createFileRoute('/merge-middleware-context')({ + component: MergeMiddlewareContext, +}) diff --git a/packages/vue-router/src/index.tsx b/packages/vue-router/src/index.tsx index 459d50f82c3..3e6b1615161 100644 --- a/packages/vue-router/src/index.tsx +++ b/packages/vue-router/src/index.tsx @@ -219,6 +219,7 @@ export type { ActiveLinkOptions, LinkProps, LinkComponent, + LinkComponentRoute, LinkComponentProps, CreateLinkProps, } from './link' diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 45ec178cbda..806fee28d52 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -630,6 +630,26 @@ export type LinkComponent = < props: LinkComponentProps, ) => Vue.VNode +export interface LinkComponentRoute< + in out TDefaultFrom extends string = string, +> { + defaultFrom: TDefaultFrom + < + TRouter extends AnyRouter = RegisteredRouter, + const TTo extends string | undefined = undefined, + const TMaskTo extends string = '', + >( + props: LinkComponentProps< + 'a', + TRouter, + this['defaultFrom'], + TTo, + this['defaultFrom'], + TMaskTo + >, + ): Vue.VNode +} + export function createLink( Comp: Constrain Vue.VNode>, ): LinkComponent { diff --git a/packages/vue-router/src/route.ts b/packages/vue-router/src/route.ts index 458d70659d9..36b6d978972 100644 --- a/packages/vue-router/src/route.ts +++ b/packages/vue-router/src/route.ts @@ -4,6 +4,7 @@ import { BaseRouteApi, notFound, } from '@tanstack/router-core' +import { Link } from './link' import { useLoaderData } from './useLoaderData' import { useLoaderDeps } from './useLoaderDeps' import { useParams } from './useParams' @@ -42,8 +43,9 @@ import type { UseMatchRoute } from './useMatch' import type { UseLoaderDepsRoute } from './useLoaderDeps' import type { UseParamsRoute } from './useParams' import type { UseSearchRoute } from './useSearch' -import type * as Vue from 'vue' +import * as Vue from 'vue' import type { UseRouteContextRoute } from './useRouteContext' +import type { LinkComponentRoute } from './link' // Structural type for Vue SFC components (.vue files) // Uses structural matching to accept Vue components without breaking @@ -73,6 +75,7 @@ declare module '@tanstack/router-core' { useLoaderDeps: UseLoaderDepsRoute useLoaderData: UseLoaderDataRoute useNavigate: () => UseNavigateResult + Link: LinkComponentRoute } } @@ -140,6 +143,15 @@ export class RouteApi< notFound = (opts?: NotFoundError) => { return notFound({ routeId: this.id as string, ...opts }) } + + Link: LinkComponentRoute['fullPath']> = (( + props, + ctx?: Vue.SetupContext, + ) => { + const router = useRouter() + const fullPath = router.routesById[this.id as string].fullPath + return Vue.h(Link as any, { from: fullPath as never, ...(props as any) }, ctx?.slots) + }) as LinkComponentRoute['fullPath']> } export class Route< @@ -277,6 +289,14 @@ export class Route< useNavigate = (): UseNavigateResult => { return useNavigate({ from: this.fullPath }) } + + Link: LinkComponentRoute = ((props, ctx?: Vue.SetupContext) => { + return Vue.h( + Link as any, + { from: this.fullPath as never, ...(props as any) }, + ctx?.slots, + ) + }) as LinkComponentRoute } export function createRoute< @@ -515,6 +535,14 @@ export class RootRoute< useNavigate = (): UseNavigateResult<'/'> => { return useNavigate({ from: this.fullPath }) } + + Link: LinkComponentRoute<'/'> = ((props, ctx?: Vue.SetupContext) => { + return Vue.h( + Link as any, + { from: this.fullPath as never, ...(props as any) }, + ctx?.slots, + ) + }) as LinkComponentRoute<'/'> } export function createRouteMask< From 0b31ea6019dfcfe0c7471dda54a7dcf3eea79b6f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:41:48 +0000 Subject: [PATCH 5/6] ci: apply automated fixes --- .../server-routes/src/routes/merge-middleware-context.tsx | 3 ++- packages/vue-router/src/route.ts | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx b/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx index 76eac749ac7..c0f655a4dad 100644 --- a/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx +++ b/e2e/vue-start/server-routes/src/routes/merge-middleware-context.tsx @@ -58,7 +58,8 @@ const MergeMiddlewareContext = defineComponent({
- Has test: {apiResponse.value?.context?.test ? 'true' : 'false'} + Has test:{' '} + {apiResponse.value?.context?.test ? 'true' : 'false'}
diff --git a/packages/vue-router/src/route.ts b/packages/vue-router/src/route.ts index 36b6d978972..ec13b075261 100644 --- a/packages/vue-router/src/route.ts +++ b/packages/vue-router/src/route.ts @@ -150,7 +150,11 @@ export class RouteApi< ) => { const router = useRouter() const fullPath = router.routesById[this.id as string].fullPath - return Vue.h(Link as any, { from: fullPath as never, ...(props as any) }, ctx?.slots) + return Vue.h( + Link as any, + { from: fullPath as never, ...(props as any) }, + ctx?.slots, + ) }) as LinkComponentRoute['fullPath']> } From 488e3367a932354a23a75f9e140c12c487f59c88 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 19:53:32 +0100 Subject: [PATCH 6/6] lint --- packages/vue-router/src/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-router/src/route.ts b/packages/vue-router/src/route.ts index ec13b075261..95295eceb00 100644 --- a/packages/vue-router/src/route.ts +++ b/packages/vue-router/src/route.ts @@ -4,6 +4,7 @@ import { BaseRouteApi, notFound, } from '@tanstack/router-core' +import * as Vue from 'vue' import { Link } from './link' import { useLoaderData } from './useLoaderData' import { useLoaderDeps } from './useLoaderDeps' @@ -43,7 +44,6 @@ import type { UseMatchRoute } from './useMatch' import type { UseLoaderDepsRoute } from './useLoaderDeps' import type { UseParamsRoute } from './useParams' import type { UseSearchRoute } from './useSearch' -import * as Vue from 'vue' import type { UseRouteContextRoute } from './useRouteContext' import type { LinkComponentRoute } from './link'