diff --git a/e2e/vue-start/server-functions/.gitignore b/e2e/vue-start/server-functions/.gitignore
new file mode 100644
index 00000000000..a79d5cf1299
--- /dev/null
+++ b/e2e/vue-start/server-functions/.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-functions/.prettierignore b/e2e/vue-start/server-functions/.prettierignore
new file mode 100644
index 00000000000..2be5eaa6ece
--- /dev/null
+++ b/e2e/vue-start/server-functions/.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-functions/package.json b/e2e/vue-start/server-functions/package.json
new file mode 100644
index 00000000000..27faff7ff22
--- /dev/null
+++ b/e2e/vue-start/server-functions/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "tanstack-vue-start-e2e-server-functions",
+ "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": "rm -rf port*.txt; 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.9.8",
+ "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-functions/playwright.config.ts b/e2e/vue-start/server-functions/playwright.config.ts
new file mode 100644
index 00000000000..cb1da03b942
--- /dev/null
+++ b/e2e/vue-start/server-functions/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-functions/postcss.config.mjs b/e2e/vue-start/server-functions/postcss.config.mjs
new file mode 100644
index 00000000000..a7f73a2d1d7
--- /dev/null
+++ b/e2e/vue-start/server-functions/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
diff --git a/e2e/vue-start/server-functions/public/favicon.ico b/e2e/vue-start/server-functions/public/favicon.ico
new file mode 100644
index 00000000000..1a1751676f7
Binary files /dev/null and b/e2e/vue-start/server-functions/public/favicon.ico differ
diff --git a/e2e/vue-start/server-functions/public/favicon.png b/e2e/vue-start/server-functions/public/favicon.png
new file mode 100644
index 00000000000..1e77bc06091
Binary files /dev/null and b/e2e/vue-start/server-functions/public/favicon.png differ
diff --git a/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx b/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx
new file mode 100644
index 00000000000..b1f818dd747
--- /dev/null
+++ b/e2e/vue-start/server-functions/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 (
+
+
+
+ {
+ router.invalidate()
+ }}
+ class={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
+ >
+ Try Again
+
+ {isRoot.value ? (
+
+ Home
+
+ ) : (
+ {
+ e.preventDefault()
+ window.history.back()
+ }}
+ >
+ Go Back
+
+ )}
+
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/components/NotFound.tsx b/e2e/vue-start/server-functions/src/components/NotFound.tsx
new file mode 100644
index 00000000000..944e35c12c6
--- /dev/null
+++ b/e2e/vue-start/server-functions/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.
}
+
+
+ window.history.back()}
+ class="bg-emerald-500 text-white px-2 py-1 rounded-sm uppercase font-black text-sm"
+ >
+ Go back
+
+
+ Start Over
+
+
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routeTree.gen.ts b/e2e/vue-start/server-functions/src/routeTree.gen.ts
new file mode 100644
index 00000000000..91633bf3c79
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routeTree.gen.ts
@@ -0,0 +1,621 @@
+/* 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 SubmitPostFormdataRouteImport } from './routes/submit-post-formdata'
+import { Route as StatusRouteImport } from './routes/status'
+import { Route as SerializeFormDataRouteImport } from './routes/serialize-form-data'
+import { Route as ReturnNullRouteImport } from './routes/return-null'
+import { Route as RawResponseRouteImport } from './routes/raw-response'
+import { Route as MultipartRouteImport } from './routes/multipart'
+import { Route as IsomorphicFnsRouteImport } from './routes/isomorphic-fns'
+import { Route as HeadersRouteImport } from './routes/headers'
+import { Route as EnvOnlyRouteImport } from './routes/env-only'
+import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserve'
+import { Route as ConsistentRouteImport } from './routes/consistent'
+import { Route as AbortSignalRouteImport } from './routes/abort-signal'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as RedirectTestIndexRouteImport } from './routes/redirect-test/index'
+import { Route as RedirectTestSsrIndexRouteImport } from './routes/redirect-test-ssr/index'
+import { Route as PrimitivesIndexRouteImport } from './routes/primitives/index'
+import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index'
+import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index'
+import { Route as FactoryIndexRouteImport } from './routes/factory/index'
+import { Route as CookiesIndexRouteImport } from './routes/cookies/index'
+import { Route as RedirectTestTargetRouteImport } from './routes/redirect-test/target'
+import { Route as RedirectTestSsrTargetRouteImport } from './routes/redirect-test-ssr/target'
+import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn'
+import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware'
+import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router'
+import { Route as CookiesSetRouteImport } from './routes/cookies/set'
+import { Route as FormdataRedirectTargetNameRouteImport } from './routes/formdata-redirect/target.$name'
+
+const SubmitPostFormdataRoute = SubmitPostFormdataRouteImport.update({
+ id: '/submit-post-formdata',
+ path: '/submit-post-formdata',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const StatusRoute = StatusRouteImport.update({
+ id: '/status',
+ path: '/status',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const SerializeFormDataRoute = SerializeFormDataRouteImport.update({
+ id: '/serialize-form-data',
+ path: '/serialize-form-data',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ReturnNullRoute = ReturnNullRouteImport.update({
+ id: '/return-null',
+ path: '/return-null',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RawResponseRoute = RawResponseRouteImport.update({
+ id: '/raw-response',
+ path: '/raw-response',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MultipartRoute = MultipartRouteImport.update({
+ id: '/multipart',
+ path: '/multipart',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IsomorphicFnsRoute = IsomorphicFnsRouteImport.update({
+ id: '/isomorphic-fns',
+ path: '/isomorphic-fns',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const HeadersRoute = HeadersRouteImport.update({
+ id: '/headers',
+ path: '/headers',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const EnvOnlyRoute = EnvOnlyRouteImport.update({
+ id: '/env-only',
+ path: '/env-only',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const DeadCodePreserveRoute = DeadCodePreserveRouteImport.update({
+ id: '/dead-code-preserve',
+ path: '/dead-code-preserve',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ConsistentRoute = ConsistentRouteImport.update({
+ id: '/consistent',
+ path: '/consistent',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const AbortSignalRoute = AbortSignalRouteImport.update({
+ id: '/abort-signal',
+ path: '/abort-signal',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RedirectTestIndexRoute = RedirectTestIndexRouteImport.update({
+ id: '/redirect-test/',
+ path: '/redirect-test/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RedirectTestSsrIndexRoute = RedirectTestSsrIndexRouteImport.update({
+ id: '/redirect-test-ssr/',
+ path: '/redirect-test-ssr/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PrimitivesIndexRoute = PrimitivesIndexRouteImport.update({
+ id: '/primitives/',
+ path: '/primitives/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MiddlewareIndexRoute = MiddlewareIndexRouteImport.update({
+ id: '/middleware/',
+ path: '/middleware/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FormdataRedirectIndexRoute = FormdataRedirectIndexRouteImport.update({
+ id: '/formdata-redirect/',
+ path: '/formdata-redirect/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FactoryIndexRoute = FactoryIndexRouteImport.update({
+ id: '/factory/',
+ path: '/factory/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const CookiesIndexRoute = CookiesIndexRouteImport.update({
+ id: '/cookies/',
+ path: '/cookies/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RedirectTestTargetRoute = RedirectTestTargetRouteImport.update({
+ id: '/redirect-test/target',
+ path: '/redirect-test/target',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RedirectTestSsrTargetRoute = RedirectTestSsrTargetRouteImport.update({
+ id: '/redirect-test-ssr/target',
+ path: '/redirect-test-ssr/target',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({
+ id: '/middleware/send-serverFn',
+ path: '/middleware/send-serverFn',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const MiddlewareRequestMiddlewareRoute =
+ MiddlewareRequestMiddlewareRouteImport.update({
+ id: '/middleware/request-middleware',
+ path: '/middleware/request-middleware',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const MiddlewareClientMiddlewareRouterRoute =
+ MiddlewareClientMiddlewareRouterRouteImport.update({
+ id: '/middleware/client-middleware-router',
+ path: '/middleware/client-middleware-router',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+const CookiesSetRoute = CookiesSetRouteImport.update({
+ id: '/cookies/set',
+ path: '/cookies/set',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const FormdataRedirectTargetNameRoute =
+ FormdataRedirectTargetNameRouteImport.update({
+ id: '/formdata-redirect/target/$name',
+ path: '/formdata-redirect/target/$name',
+ getParentRoute: () => rootRouteImport,
+ } as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/abort-signal': typeof AbortSignalRoute
+ '/consistent': typeof ConsistentRoute
+ '/dead-code-preserve': typeof DeadCodePreserveRoute
+ '/env-only': typeof EnvOnlyRoute
+ '/headers': typeof HeadersRoute
+ '/isomorphic-fns': typeof IsomorphicFnsRoute
+ '/multipart': typeof MultipartRoute
+ '/raw-response': typeof RawResponseRoute
+ '/return-null': typeof ReturnNullRoute
+ '/serialize-form-data': typeof SerializeFormDataRoute
+ '/status': typeof StatusRoute
+ '/submit-post-formdata': typeof SubmitPostFormdataRoute
+ '/cookies/set': typeof CookiesSetRoute
+ '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
+ '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
+ '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
+ '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute
+ '/redirect-test/target': typeof RedirectTestTargetRoute
+ '/cookies': typeof CookiesIndexRoute
+ '/factory': typeof FactoryIndexRoute
+ '/formdata-redirect': typeof FormdataRedirectIndexRoute
+ '/middleware': typeof MiddlewareIndexRoute
+ '/primitives': typeof PrimitivesIndexRoute
+ '/redirect-test-ssr': typeof RedirectTestSsrIndexRoute
+ '/redirect-test': typeof RedirectTestIndexRoute
+ '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/abort-signal': typeof AbortSignalRoute
+ '/consistent': typeof ConsistentRoute
+ '/dead-code-preserve': typeof DeadCodePreserveRoute
+ '/env-only': typeof EnvOnlyRoute
+ '/headers': typeof HeadersRoute
+ '/isomorphic-fns': typeof IsomorphicFnsRoute
+ '/multipart': typeof MultipartRoute
+ '/raw-response': typeof RawResponseRoute
+ '/return-null': typeof ReturnNullRoute
+ '/serialize-form-data': typeof SerializeFormDataRoute
+ '/status': typeof StatusRoute
+ '/submit-post-formdata': typeof SubmitPostFormdataRoute
+ '/cookies/set': typeof CookiesSetRoute
+ '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
+ '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
+ '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
+ '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute
+ '/redirect-test/target': typeof RedirectTestTargetRoute
+ '/cookies': typeof CookiesIndexRoute
+ '/factory': typeof FactoryIndexRoute
+ '/formdata-redirect': typeof FormdataRedirectIndexRoute
+ '/middleware': typeof MiddlewareIndexRoute
+ '/primitives': typeof PrimitivesIndexRoute
+ '/redirect-test-ssr': typeof RedirectTestSsrIndexRoute
+ '/redirect-test': typeof RedirectTestIndexRoute
+ '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/abort-signal': typeof AbortSignalRoute
+ '/consistent': typeof ConsistentRoute
+ '/dead-code-preserve': typeof DeadCodePreserveRoute
+ '/env-only': typeof EnvOnlyRoute
+ '/headers': typeof HeadersRoute
+ '/isomorphic-fns': typeof IsomorphicFnsRoute
+ '/multipart': typeof MultipartRoute
+ '/raw-response': typeof RawResponseRoute
+ '/return-null': typeof ReturnNullRoute
+ '/serialize-form-data': typeof SerializeFormDataRoute
+ '/status': typeof StatusRoute
+ '/submit-post-formdata': typeof SubmitPostFormdataRoute
+ '/cookies/set': typeof CookiesSetRoute
+ '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
+ '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
+ '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
+ '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute
+ '/redirect-test/target': typeof RedirectTestTargetRoute
+ '/cookies/': typeof CookiesIndexRoute
+ '/factory/': typeof FactoryIndexRoute
+ '/formdata-redirect/': typeof FormdataRedirectIndexRoute
+ '/middleware/': typeof MiddlewareIndexRoute
+ '/primitives/': typeof PrimitivesIndexRoute
+ '/redirect-test-ssr/': typeof RedirectTestSsrIndexRoute
+ '/redirect-test/': typeof RedirectTestIndexRoute
+ '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/abort-signal'
+ | '/consistent'
+ | '/dead-code-preserve'
+ | '/env-only'
+ | '/headers'
+ | '/isomorphic-fns'
+ | '/multipart'
+ | '/raw-response'
+ | '/return-null'
+ | '/serialize-form-data'
+ | '/status'
+ | '/submit-post-formdata'
+ | '/cookies/set'
+ | '/middleware/client-middleware-router'
+ | '/middleware/request-middleware'
+ | '/middleware/send-serverFn'
+ | '/redirect-test-ssr/target'
+ | '/redirect-test/target'
+ | '/cookies'
+ | '/factory'
+ | '/formdata-redirect'
+ | '/middleware'
+ | '/primitives'
+ | '/redirect-test-ssr'
+ | '/redirect-test'
+ | '/formdata-redirect/target/$name'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/abort-signal'
+ | '/consistent'
+ | '/dead-code-preserve'
+ | '/env-only'
+ | '/headers'
+ | '/isomorphic-fns'
+ | '/multipart'
+ | '/raw-response'
+ | '/return-null'
+ | '/serialize-form-data'
+ | '/status'
+ | '/submit-post-formdata'
+ | '/cookies/set'
+ | '/middleware/client-middleware-router'
+ | '/middleware/request-middleware'
+ | '/middleware/send-serverFn'
+ | '/redirect-test-ssr/target'
+ | '/redirect-test/target'
+ | '/cookies'
+ | '/factory'
+ | '/formdata-redirect'
+ | '/middleware'
+ | '/primitives'
+ | '/redirect-test-ssr'
+ | '/redirect-test'
+ | '/formdata-redirect/target/$name'
+ id:
+ | '__root__'
+ | '/'
+ | '/abort-signal'
+ | '/consistent'
+ | '/dead-code-preserve'
+ | '/env-only'
+ | '/headers'
+ | '/isomorphic-fns'
+ | '/multipart'
+ | '/raw-response'
+ | '/return-null'
+ | '/serialize-form-data'
+ | '/status'
+ | '/submit-post-formdata'
+ | '/cookies/set'
+ | '/middleware/client-middleware-router'
+ | '/middleware/request-middleware'
+ | '/middleware/send-serverFn'
+ | '/redirect-test-ssr/target'
+ | '/redirect-test/target'
+ | '/cookies/'
+ | '/factory/'
+ | '/formdata-redirect/'
+ | '/middleware/'
+ | '/primitives/'
+ | '/redirect-test-ssr/'
+ | '/redirect-test/'
+ | '/formdata-redirect/target/$name'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ AbortSignalRoute: typeof AbortSignalRoute
+ ConsistentRoute: typeof ConsistentRoute
+ DeadCodePreserveRoute: typeof DeadCodePreserveRoute
+ EnvOnlyRoute: typeof EnvOnlyRoute
+ HeadersRoute: typeof HeadersRoute
+ IsomorphicFnsRoute: typeof IsomorphicFnsRoute
+ MultipartRoute: typeof MultipartRoute
+ RawResponseRoute: typeof RawResponseRoute
+ ReturnNullRoute: typeof ReturnNullRoute
+ SerializeFormDataRoute: typeof SerializeFormDataRoute
+ StatusRoute: typeof StatusRoute
+ SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute
+ CookiesSetRoute: typeof CookiesSetRoute
+ MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
+ MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute
+ MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute
+ RedirectTestSsrTargetRoute: typeof RedirectTestSsrTargetRoute
+ RedirectTestTargetRoute: typeof RedirectTestTargetRoute
+ CookiesIndexRoute: typeof CookiesIndexRoute
+ FactoryIndexRoute: typeof FactoryIndexRoute
+ FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute
+ MiddlewareIndexRoute: typeof MiddlewareIndexRoute
+ PrimitivesIndexRoute: typeof PrimitivesIndexRoute
+ RedirectTestSsrIndexRoute: typeof RedirectTestSsrIndexRoute
+ RedirectTestIndexRoute: typeof RedirectTestIndexRoute
+ FormdataRedirectTargetNameRoute: typeof FormdataRedirectTargetNameRoute
+}
+
+declare module '@tanstack/vue-router' {
+ interface FileRoutesByPath {
+ '/submit-post-formdata': {
+ id: '/submit-post-formdata'
+ path: '/submit-post-formdata'
+ fullPath: '/submit-post-formdata'
+ preLoaderRoute: typeof SubmitPostFormdataRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/status': {
+ id: '/status'
+ path: '/status'
+ fullPath: '/status'
+ preLoaderRoute: typeof StatusRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/serialize-form-data': {
+ id: '/serialize-form-data'
+ path: '/serialize-form-data'
+ fullPath: '/serialize-form-data'
+ preLoaderRoute: typeof SerializeFormDataRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/return-null': {
+ id: '/return-null'
+ path: '/return-null'
+ fullPath: '/return-null'
+ preLoaderRoute: typeof ReturnNullRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/raw-response': {
+ id: '/raw-response'
+ path: '/raw-response'
+ fullPath: '/raw-response'
+ preLoaderRoute: typeof RawResponseRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/multipart': {
+ id: '/multipart'
+ path: '/multipart'
+ fullPath: '/multipart'
+ preLoaderRoute: typeof MultipartRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/isomorphic-fns': {
+ id: '/isomorphic-fns'
+ path: '/isomorphic-fns'
+ fullPath: '/isomorphic-fns'
+ preLoaderRoute: typeof IsomorphicFnsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/headers': {
+ id: '/headers'
+ path: '/headers'
+ fullPath: '/headers'
+ preLoaderRoute: typeof HeadersRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/env-only': {
+ id: '/env-only'
+ path: '/env-only'
+ fullPath: '/env-only'
+ preLoaderRoute: typeof EnvOnlyRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/dead-code-preserve': {
+ id: '/dead-code-preserve'
+ path: '/dead-code-preserve'
+ fullPath: '/dead-code-preserve'
+ preLoaderRoute: typeof DeadCodePreserveRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/consistent': {
+ id: '/consistent'
+ path: '/consistent'
+ fullPath: '/consistent'
+ preLoaderRoute: typeof ConsistentRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/abort-signal': {
+ id: '/abort-signal'
+ path: '/abort-signal'
+ fullPath: '/abort-signal'
+ preLoaderRoute: typeof AbortSignalRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/redirect-test/': {
+ id: '/redirect-test/'
+ path: '/redirect-test'
+ fullPath: '/redirect-test'
+ preLoaderRoute: typeof RedirectTestIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/redirect-test-ssr/': {
+ id: '/redirect-test-ssr/'
+ path: '/redirect-test-ssr'
+ fullPath: '/redirect-test-ssr'
+ preLoaderRoute: typeof RedirectTestSsrIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/primitives/': {
+ id: '/primitives/'
+ path: '/primitives'
+ fullPath: '/primitives'
+ preLoaderRoute: typeof PrimitivesIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/middleware/': {
+ id: '/middleware/'
+ path: '/middleware'
+ fullPath: '/middleware'
+ preLoaderRoute: typeof MiddlewareIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/formdata-redirect/': {
+ id: '/formdata-redirect/'
+ path: '/formdata-redirect'
+ fullPath: '/formdata-redirect'
+ preLoaderRoute: typeof FormdataRedirectIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/factory/': {
+ id: '/factory/'
+ path: '/factory'
+ fullPath: '/factory'
+ preLoaderRoute: typeof FactoryIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/cookies/': {
+ id: '/cookies/'
+ path: '/cookies'
+ fullPath: '/cookies'
+ preLoaderRoute: typeof CookiesIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/redirect-test/target': {
+ id: '/redirect-test/target'
+ path: '/redirect-test/target'
+ fullPath: '/redirect-test/target'
+ preLoaderRoute: typeof RedirectTestTargetRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/redirect-test-ssr/target': {
+ id: '/redirect-test-ssr/target'
+ path: '/redirect-test-ssr/target'
+ fullPath: '/redirect-test-ssr/target'
+ preLoaderRoute: typeof RedirectTestSsrTargetRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/middleware/send-serverFn': {
+ id: '/middleware/send-serverFn'
+ path: '/middleware/send-serverFn'
+ fullPath: '/middleware/send-serverFn'
+ preLoaderRoute: typeof MiddlewareSendServerFnRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/middleware/request-middleware': {
+ id: '/middleware/request-middleware'
+ path: '/middleware/request-middleware'
+ fullPath: '/middleware/request-middleware'
+ preLoaderRoute: typeof MiddlewareRequestMiddlewareRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/middleware/client-middleware-router': {
+ id: '/middleware/client-middleware-router'
+ path: '/middleware/client-middleware-router'
+ fullPath: '/middleware/client-middleware-router'
+ preLoaderRoute: typeof MiddlewareClientMiddlewareRouterRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/cookies/set': {
+ id: '/cookies/set'
+ path: '/cookies/set'
+ fullPath: '/cookies/set'
+ preLoaderRoute: typeof CookiesSetRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/formdata-redirect/target/$name': {
+ id: '/formdata-redirect/target/$name'
+ path: '/formdata-redirect/target/$name'
+ fullPath: '/formdata-redirect/target/$name'
+ preLoaderRoute: typeof FormdataRedirectTargetNameRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ AbortSignalRoute: AbortSignalRoute,
+ ConsistentRoute: ConsistentRoute,
+ DeadCodePreserveRoute: DeadCodePreserveRoute,
+ EnvOnlyRoute: EnvOnlyRoute,
+ HeadersRoute: HeadersRoute,
+ IsomorphicFnsRoute: IsomorphicFnsRoute,
+ MultipartRoute: MultipartRoute,
+ RawResponseRoute: RawResponseRoute,
+ ReturnNullRoute: ReturnNullRoute,
+ SerializeFormDataRoute: SerializeFormDataRoute,
+ StatusRoute: StatusRoute,
+ SubmitPostFormdataRoute: SubmitPostFormdataRoute,
+ CookiesSetRoute: CookiesSetRoute,
+ MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
+ MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute,
+ MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
+ RedirectTestSsrTargetRoute: RedirectTestSsrTargetRoute,
+ RedirectTestTargetRoute: RedirectTestTargetRoute,
+ CookiesIndexRoute: CookiesIndexRoute,
+ FactoryIndexRoute: FactoryIndexRoute,
+ FormdataRedirectIndexRoute: FormdataRedirectIndexRoute,
+ MiddlewareIndexRoute: MiddlewareIndexRoute,
+ PrimitivesIndexRoute: PrimitivesIndexRoute,
+ RedirectTestSsrIndexRoute: RedirectTestSsrIndexRoute,
+ RedirectTestIndexRoute: RedirectTestIndexRoute,
+ FormdataRedirectTargetNameRoute: FormdataRedirectTargetNameRoute,
+}
+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-functions/src/router.tsx b/e2e/vue-start/server-functions/src/router.tsx
new file mode 100644
index 00000000000..11122f6b8a9
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/router.tsx
@@ -0,0 +1,32 @@
+import { createRouter } from '@tanstack/vue-router'
+import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query'
+import { QueryClient } from '@tanstack/vue-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,
+ context: {
+ foo: {
+ bar: 'baz',
+ },
+ },
+ })
+
+ setupRouterSsrQueryIntegration({ router, queryClient })
+
+ return router
+}
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: ReturnType
+ }
+}
diff --git a/e2e/vue-start/server-functions/src/routes/__root.tsx b/e2e/vue-start/server-functions/src/routes/__root.tsx
new file mode 100644
index 00000000000..342f7a79b39
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/__root.tsx
@@ -0,0 +1,47 @@
+import {
+ Body,
+ HeadContent,
+ Html,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/vue-router'
+
+import { TanStackRouterDevtoolsInProd } 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-functions/src/routes/abort-signal.tsx b/e2e/vue-start/server-functions/src/routes/abort-signal.tsx
new file mode 100644
index 00000000000..2a3e08ae593
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/abort-signal.tsx
@@ -0,0 +1,86 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const abortableServerFn = createServerFn().handler(
+ async ({ context, signal }) => {
+ console.log('server function started', { context, signal })
+ return new Promise((resolve, reject) => {
+ if (signal.aborted) {
+ return reject(new Error('Aborted before start'))
+ }
+ const timerId = setTimeout(() => {
+ console.log('server function finished')
+ resolve('server function result')
+ }, 1000)
+ const onAbort = () => {
+ clearTimeout(timerId)
+ console.log('server function aborted')
+ reject(new Error('Aborted'))
+ }
+ signal.addEventListener('abort', onAbort, { once: true })
+ })
+ },
+)
+
+const RouteComponent = defineComponent({
+ setup() {
+ const errorMessage = ref(undefined)
+ const result = ref(undefined)
+
+ const reset = () => {
+ errorMessage.value = undefined
+ result.value = undefined
+ }
+
+ return () => (
+
+
{
+ reset()
+ const controller = new AbortController()
+ const serverFnPromise = abortableServerFn({
+ signal: controller.signal,
+ })
+ const timeoutPromise = new Promise((resolve) =>
+ setTimeout(resolve, 500),
+ )
+ await timeoutPromise
+ controller.abort()
+ try {
+ const serverFnResult = await serverFnPromise
+ result.value = serverFnResult
+ } catch (error) {
+ errorMessage.value = (error as any).message
+ }
+ }}
+ >
+ call server function with abort signal
+
+
+
{
+ reset()
+ const serverFnResult = await abortableServerFn()
+ result.value = serverFnResult
+ }}
+ >
+ call server function
+
+
+ result:
{result.value ?? '$undefined'}
+
+
+ message:{' '}
+
{errorMessage.value ?? '$undefined'}
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/abort-signal')({
+ component: RouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/consistent.tsx b/e2e/vue-start/server-functions/src/routes/consistent.tsx
new file mode 100644
index 00000000000..5e6b0a7e08e
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/consistent.tsx
@@ -0,0 +1,129 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+/**
+ * This checks whether the returned payloads from a
+ * server function are the same, regardless of whether the server function is
+ * called directly from the client or from within the server function.
+ * @link https://github.com/TanStack/router/issues/1866
+ * @link https://github.com/TanStack/router/issues/2481
+ */
+
+const cons_getFn1 = createServerFn()
+ .inputValidator((d: { username: string }) => d)
+ .handler(({ data }) => {
+ return { payload: data }
+ })
+
+const cons_serverGetFn1 = createServerFn()
+ .inputValidator((d: { username: string }) => d)
+ .handler(async ({ data }) => {
+ return cons_getFn1({ data })
+ })
+
+const cons_postFn1 = createServerFn({ method: 'POST' })
+ .inputValidator((d: { username: string }) => d)
+ .handler(({ data }) => {
+ return { payload: data }
+ })
+
+const cons_serverPostFn1 = createServerFn({ method: 'POST' })
+ .inputValidator((d: { username: string }) => d)
+ .handler(({ data }) => {
+ return cons_postFn1({ data })
+ })
+
+const ConsistentServerFnCalls = defineComponent({
+ setup() {
+ const getServerResult = ref({})
+ const getDirectResult = ref({})
+
+ const postServerResult = ref({})
+ const postDirectResult = ref({})
+
+ return () => (
+
+
Consistent Server Fn GET Calls
+
+ This component checks whether the returned payloads from server
+ function are the same, regardless of whether the server function is
+ called directly from the client or from within the server function.
+
+
+ It should return{' '}
+
+
+ {JSON.stringify({ payload: { username: 'TEST' } })}
+
+
+
+
+ {`GET: cons_getFn1 called from server cons_serverGetFn1 returns`}
+
+
+ {JSON.stringify(getServerResult.value)}
+
+
+
+ {`GET: cons_getFn1 called directly returns`}
+
+
+ {JSON.stringify(getDirectResult.value)}
+
+
+
+ {`POST: cons_postFn1 called from cons_serverPostFn1 returns`}
+
+
+ {JSON.stringify(postServerResult.value)}
+
+
+
+ {`POST: cons_postFn1 called directly returns`}
+
+
+ {JSON.stringify(postDirectResult.value)}
+
+
+
{
+ // GET calls
+ cons_serverGetFn1({ data: { username: 'TEST' } }).then((data) => {
+ getServerResult.value = data
+ })
+ cons_getFn1({ data: { username: 'TEST' } }).then((data) => {
+ getDirectResult.value = data
+ })
+
+ // POST calls
+ cons_serverPostFn1({ data: { username: 'TEST' } }).then((data) => {
+ postServerResult.value = data
+ })
+ cons_postFn1({ data: { username: 'TEST' } }).then((data) => {
+ postDirectResult.value = data
+ })
+
+ cons_postFn1({ data: { username: 'TEST' } }).then((data) => {
+ postDirectResult.value = data
+ })
+ }}
+ >
+ Test Consistent server function responses
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/consistent')({
+ component: ConsistentServerFnCalls,
+ loader: async () => {
+ const data = await cons_serverGetFn1({ data: { username: 'TEST' } })
+ console.log('cons_serverGetFn1', data)
+ return { data }
+ },
+})
diff --git a/e2e/vue-start/server-functions/src/routes/cookies/index.tsx b/e2e/vue-start/server-functions/src/routes/cookies/index.tsx
new file mode 100644
index 00000000000..c47897c1096
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/cookies/index.tsx
@@ -0,0 +1,24 @@
+import { Link, createFileRoute } from '@tanstack/vue-router'
+import { z } from 'zod'
+
+const cookieSchema = z
+ .object({ value: z.string() })
+ .catch(() => ({ value: `CLIENT-${Date.now()}` }))
+export const Route = createFileRoute('/cookies/')({
+ validateSearch: cookieSchema,
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const search = Route.useSearch()
+ return (
+
+ got to route that sets the cookies with {JSON.stringify(search.value)}
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/cookies/set.tsx b/e2e/vue-start/server-functions/src/routes/cookies/set.tsx
new file mode 100644
index 00000000000..39fd00b74e4
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/cookies/set.tsx
@@ -0,0 +1,78 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { setCookie } from '@tanstack/vue-start/server'
+import { z } from 'zod'
+import Cookies from 'js-cookie'
+import { defineComponent, ref, watch } from 'vue'
+
+const cookieSchema = z.object({ value: z.string() })
+
+export const setCookieServerFn1 = createServerFn()
+ .inputValidator(cookieSchema)
+ .handler(({ data }) => {
+ setCookie(`cookie-1-${data.value}`, data.value)
+ setCookie(`cookie-2-${data.value}`, data.value)
+ })
+
+export const setCookieServerFn2 = createServerFn()
+ .inputValidator(cookieSchema)
+ .handler(({ data }) => {
+ setCookie(`cookie-3-${data.value}`, data.value)
+ setCookie(`cookie-4-${data.value}`, data.value)
+ })
+
+const RouteComponent = defineComponent({
+ setup() {
+ const search = Route.useSearch()
+ const cookiesFromDocument = ref>({})
+
+ const updateCookies = () => {
+ const tempCookies: Record = {}
+ for (let i = 1; i <= 4; i++) {
+ const key = `cookie-${i}-${search.value.value}`
+ tempCookies[key] = Cookies.get(key)
+ }
+ cookiesFromDocument.value = tempCookies
+ }
+
+ if (typeof window !== 'undefined') {
+ watch(
+ () => search.value.value,
+ () => {
+ updateCookies()
+ },
+ { immediate: true },
+ )
+ }
+
+ return () => (
+
+
cookies result
+
+
+
+ cookie
+ value
+
+ {Object.entries(cookiesFromDocument.value).map(([key, value]) => (
+
+ {key}
+ {value}
+
+ ))}
+
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/cookies/set')({
+ validateSearch: cookieSchema,
+ loaderDeps: ({ search }) => search,
+ loader: async ({ deps }) => {
+ await setCookieServerFn1({ data: deps })
+ await setCookieServerFn2({ data: deps })
+ },
+ component: RouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx
new file mode 100644
index 00000000000..b80c6bf7701
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx
@@ -0,0 +1,64 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import * as fs from 'node:fs'
+import { createServerFn } from '@tanstack/vue-start'
+import { getRequestHeader } from '@tanstack/vue-start/server'
+import { defineComponent, ref } from 'vue'
+
+// by using this we make sure DCE still works - this errors when imported on the client
+
+const filePath = 'count-effect.txt'
+
+async function readCount() {
+ return parseInt(
+ await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
+ )
+}
+
+async function updateCount() {
+ const count = await readCount()
+ await fs.promises.writeFile(filePath, `${count + 1}`)
+ return true
+}
+
+const writeFileServerFn = createServerFn().handler(async () => {
+ // eslint-disable-next-line unused-imports/no-unused-vars
+ const test = await updateCount()
+ return getRequestHeader('X-Test')
+})
+
+const readFileServerFn = createServerFn().handler(async () => {
+ const data = await readCount()
+ return data
+})
+
+const RouteComponent = defineComponent({
+ setup() {
+ const serverFnOutput = ref(undefined)
+ return () => (
+
+
Dead code test
+
+ This server function writes to a file as a side effect, then reads it.
+
+
{
+ await writeFileServerFn({ headers: { 'X-Test': 'test' } })
+ serverFnOutput.value = await readFileServerFn()
+ }}
+ >
+ Call Dead Code Fn
+
+
Server output
+
+ {serverFnOutput.value}
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/dead-code-preserve')({
+ component: RouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/env-only.tsx b/e2e/vue-start/server-functions/src/routes/env-only.tsx
new file mode 100644
index 00000000000..6b784089c79
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/env-only.tsx
@@ -0,0 +1,87 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import {
+ createClientOnlyFn,
+ createServerFn,
+ createServerOnlyFn,
+} from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const serverEcho = createServerOnlyFn((input: string) => 'server got: ' + input)
+const clientEcho = createClientOnlyFn((input: string) => 'client got: ' + input)
+
+const testOnServer = createServerFn().handler(() => {
+ const serverOnServer = serverEcho('hello')
+ let clientOnServer: string
+ try {
+ clientOnServer = clientEcho('hello')
+ } catch (e) {
+ clientOnServer =
+ 'clientEcho threw an error: ' +
+ (e instanceof Error ? e.message : String(e))
+ }
+ return { serverOnServer, clientOnServer }
+})
+
+const RouteComponent = defineComponent({
+ setup() {
+ const results = ref> | undefined>()
+
+ async function handleClick() {
+ const { serverOnServer, clientOnServer } = await testOnServer()
+ const clientOnClient = clientEcho('hello')
+ let serverOnClient: string
+ try {
+ serverOnClient = serverEcho('hello')
+ } catch (e) {
+ serverOnClient =
+ 'serverEcho threw an error: ' +
+ (e instanceof Error ? e.message : String(e))
+ }
+ results.value = {
+ serverOnServer,
+ clientOnServer,
+ clientOnClient,
+ serverOnClient,
+ }
+ }
+
+ return () => (
+
+
+ Run
+
+ {!!results.value && (
+
+
+ serverEcho
+
+ When we called the function on the server:
+
+ {results.value.serverOnServer}
+
+ When we called the function on the client:
+
+ {results.value.serverOnClient}
+
+
+
+ clientEcho
+
+ When we called the function on the server:
+
+ {results.value.clientOnServer}
+
+ When we called the function on the client:
+
+ {results.value.clientOnClient}
+
+
+ )}
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/env-only')({
+ component: RouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts
new file mode 100644
index 00000000000..bff66859175
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts
@@ -0,0 +1,22 @@
+import { createMiddleware } from '@tanstack/vue-start'
+import { createFooServerFn } from './createFooServerFn'
+
+const barMiddleware = createMiddleware({ type: 'function' }).server(
+ ({ next }) => {
+ console.log('Bar middleware triggered')
+ return next({
+ context: { bar: 'bar' } as const,
+ })
+ },
+)
+
+export const createBarServerFn = createFooServerFn().middleware([barMiddleware])
+
+export const barFnInsideFactoryFile = createBarServerFn().handler(
+ ({ context }) => {
+ return {
+ name: 'barFnInsideFactoryFile',
+ context,
+ }
+ },
+)
diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/createFakeFn.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFakeFn.ts
new file mode 100644
index 00000000000..1c727338850
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFakeFn.ts
@@ -0,0 +1,5 @@
+export function createFakeFn() {
+ return {
+ handler: (cb: () => Promise) => cb,
+ }
+}
diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts
new file mode 100644
index 00000000000..d75084b7ffb
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts
@@ -0,0 +1,24 @@
+import { createMiddleware, createServerFn } from '@tanstack/vue-start'
+import { getRequest } from '@tanstack/vue-start/server'
+
+const fooMiddleware = createMiddleware({ type: 'function' }).server(
+ ({ next }) => {
+ const request = getRequest()
+ console.log('Foo middleware triggered')
+ return next({
+ context: { foo: 'foo', method: request.method } as const,
+ })
+ },
+)
+
+export const createFooServerFn = createServerFn().middleware([fooMiddleware])
+
+export const fooFnInsideFactoryFile = createFooServerFn().handler(
+ async ({ context }) => {
+ console.log('fooFnInsideFactoryFile handler triggered', context.method)
+ return {
+ name: 'fooFnInsideFactoryFile',
+ context,
+ }
+ },
+)
diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/functions.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/functions.ts
new file mode 100644
index 00000000000..190791a5996
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/functions.ts
@@ -0,0 +1,93 @@
+import { createMiddleware, createServerFn } from '@tanstack/vue-start'
+import { createBarServerFn } from './createBarServerFn'
+import { createFooServerFn } from './createFooServerFn'
+import { createFakeFn } from './createFakeFn'
+
+export const fooFn = createFooServerFn().handler(({ context }) => {
+ return {
+ name: 'fooFn',
+ context,
+ }
+})
+
+export const fooFnPOST = createFooServerFn({ method: 'POST' }).handler(
+ ({ context }) => {
+ return {
+ name: 'fooFnPOST',
+ context,
+ }
+ },
+)
+
+export const barFn = createBarServerFn().handler(({ context }) => {
+ return {
+ name: 'barFn',
+ context,
+ }
+})
+
+export const barFnPOST = createBarServerFn({ method: 'POST' }).handler(
+ ({ context }) => {
+ return {
+ name: 'barFnPOST',
+ context,
+ }
+ },
+)
+
+const localMiddleware = createMiddleware({ type: 'function' }).server(
+ ({ next }) => {
+ console.log('local middleware triggered')
+ return next({
+ context: { local: 'local' } as const,
+ })
+ },
+)
+
+const localFnFactory = createBarServerFn.middleware([localMiddleware])
+
+const anotherMiddleware = createMiddleware({ type: 'function' }).server(
+ ({ next }) => {
+ console.log('another middleware triggered')
+ return next({
+ context: { another: 'another' } as const,
+ })
+ },
+)
+
+export const localFn = localFnFactory()
+ .middleware([anotherMiddleware])
+ .handler(({ context }) => {
+ return {
+ name: 'localFn',
+ context,
+ }
+ })
+
+export const localFnPOST = localFnFactory({ method: 'POST' })
+ .middleware([anotherMiddleware])
+ .handler(({ context }) => {
+ return {
+ name: 'localFnPOST',
+ context,
+ }
+ })
+
+export const fakeFn = createFakeFn().handler(async () => {
+ return {
+ name: 'fakeFn',
+ window,
+ }
+})
+
+export const composeFactory = createServerFn({ method: 'GET' }).middleware([
+ createBarServerFn,
+])
+export const composedFn = composeFactory()
+ .middleware([anotherMiddleware, localFnFactory])
+ .handler(({ context }) => {
+ return {
+ name: 'composedFn',
+ context,
+ }
+ })
diff --git a/e2e/vue-start/server-functions/src/routes/factory/index.tsx b/e2e/vue-start/server-functions/src/routes/factory/index.tsx
new file mode 100644
index 00000000000..8ddf7e6fbcf
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/factory/index.tsx
@@ -0,0 +1,224 @@
+import { createFileRoute, deepEqual } from '@tanstack/vue-router'
+
+import { createServerFn } from '@tanstack/vue-start'
+import { fooFnInsideFactoryFile } from './-functions/createFooServerFn'
+import {
+ barFn,
+ barFnPOST,
+ composedFn,
+ fakeFn,
+ fooFn,
+ fooFnPOST,
+ localFn,
+ localFnPOST,
+} from './-functions/functions'
+import { computed, defineComponent, ref } from 'vue'
+import type { PropType } from 'vue'
+
+const fnInsideRoute = createServerFn({ method: 'GET' }).handler(() => {
+ return {
+ name: 'fnInsideRoute',
+ }
+})
+
+const functions = {
+ fnInsideRoute: {
+ fn: fnInsideRoute,
+ type: 'serverFn',
+ expected: {
+ name: 'fnInsideRoute',
+ },
+ },
+ fooFnInsideFactoryFile: {
+ fn: fooFnInsideFactoryFile,
+ type: 'serverFn',
+
+ expected: {
+ name: 'fooFnInsideFactoryFile',
+ context: { foo: 'foo', method: 'GET' },
+ },
+ },
+ fooFn: {
+ fn: fooFn,
+ type: 'serverFn',
+
+ expected: {
+ name: 'fooFn',
+ context: { foo: 'foo', method: 'GET' },
+ },
+ },
+ fooFnPOST: {
+ fn: fooFnPOST,
+ type: 'serverFn',
+
+ expected: {
+ name: 'fooFnPOST',
+ context: { foo: 'foo', method: 'POST' },
+ },
+ },
+ barFn: {
+ fn: barFn,
+ type: 'serverFn',
+
+ expected: {
+ name: 'barFn',
+ context: { foo: 'foo', method: 'GET', bar: 'bar' },
+ },
+ },
+ barFnPOST: {
+ fn: barFnPOST,
+ type: 'serverFn',
+
+ expected: {
+ name: 'barFnPOST',
+ context: { foo: 'foo', method: 'POST', bar: 'bar' },
+ },
+ },
+ localFn: {
+ fn: localFn,
+ type: 'serverFn',
+
+ expected: {
+ name: 'localFn',
+ context: {
+ foo: 'foo',
+ method: 'GET',
+ bar: 'bar',
+ local: 'local',
+ another: 'another',
+ },
+ },
+ },
+ localFnPOST: {
+ fn: localFnPOST,
+ type: 'serverFn',
+
+ expected: {
+ name: 'localFnPOST',
+ context: {
+ foo: 'foo',
+ method: 'POST',
+ bar: 'bar',
+ local: 'local',
+ another: 'another',
+ },
+ },
+ },
+ composedFn: {
+ fn: composedFn,
+ type: 'serverFn',
+ expected: {
+ name: 'composedFn',
+ context: {
+ foo: 'foo',
+ method: 'GET',
+ bar: 'bar',
+ another: 'another',
+ local: 'local',
+ },
+ },
+ },
+ fakeFn: {
+ fn: fakeFn,
+ type: 'localFn',
+ expected: {
+ name: 'fakeFn',
+ window,
+ },
+ },
+} satisfies Record
+
+interface TestCase {
+ fn: () => Promise
+ expected: any
+ type: 'serverFn' | 'localFn'
+}
+const Test = defineComponent({
+ props: {
+ fn: {
+ type: Function as PropType<() => Promise>,
+ required: true,
+ },
+ expected: {
+ type: Object as PropType,
+ required: true,
+ },
+ type: {
+ type: String as PropType,
+ required: true,
+ },
+ },
+ setup(props) {
+ const result = ref(null)
+ const comparison = computed(() => {
+ if (result.value) {
+ const isEqual = deepEqual(result.value, props.expected)
+ return isEqual ? 'equal' : 'not equal'
+ }
+ return 'Loading...'
+ })
+
+ return () => (
+
+
+
+ It should return{' '}
+
+
+ {props.type === 'serverFn'
+ ? JSON.stringify(props.expected)
+ : 'localFn'}
+
+
+
+
+ fn returns:
+
+
+ {result.value
+ ? props.type === 'serverFn'
+ ? JSON.stringify(result.value)
+ : 'localFn'
+ : 'Loading...'}
+ {' '}
+
+ {comparison.value}
+
+
+
{
+ props.fn().then((data) => {
+ result.value = data
+ })
+ }}
+ >
+ Invoke Server Function
+
+
+ )
+ },
+})
+
+const RouteComponent = defineComponent({
+ setup() {
+ return () => (
+
+
Server functions middleware E2E tests
+ {Object.entries(functions).map(([name, testCase]) => (
+
+ ))}
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/factory/')({
+ ssr: false,
+ component: RouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx b/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx
new file mode 100644
index 00000000000..d23eb342b3f
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx
@@ -0,0 +1,74 @@
+import { createFileRoute, redirect } from '@tanstack/vue-router'
+import { createServerFn, useServerFn } from '@tanstack/vue-start'
+import { z } from 'zod'
+
+export const Route = createFileRoute('/formdata-redirect/')({
+ component: SubmitPostFormDataFn,
+ validateSearch: z.object({
+ mode: z.union([z.literal('js'), z.literal('no-js')]).default('js'),
+ }),
+})
+
+const testValues = {
+ name: 'Sean',
+}
+
+export const greetUser = createServerFn({ method: 'POST' })
+ .inputValidator((data: FormData) => {
+ if (!(data instanceof FormData)) {
+ throw new Error('Invalid! FormData is required')
+ }
+ const name = data.get('name')
+
+ if (!name) {
+ throw new Error('Name is required')
+ }
+
+ return {
+ name: name.toString(),
+ }
+ })
+ .handler(({ data: { name } }) => {
+ throw redirect({ to: '/formdata-redirect/target/$name', params: { name } })
+ })
+
+function SubmitPostFormDataFn() {
+ const mode = Route.useSearch({ select: (search) => search.mode })
+ const greetUserFn = useServerFn(greetUser)
+ return (
+
+
Submit POST FormData Fn Call
+
+ It should return redirect to /formdata-redirect/target/{testValues.name}{' '}
+ and greet the user with their name:
+
+
+ {testValues.name}
+
+
+
+
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx
new file mode 100644
index 00000000000..958aeaf6177
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/formdata-redirect/target/$name')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+
+ Hello{' '}
+
+ {params.value.name}
+
+ !
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/headers.tsx b/e2e/vue-start/server-functions/src/routes/headers.tsx
new file mode 100644
index 00000000000..9cb3d7533c7
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/headers.tsx
@@ -0,0 +1,77 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import {
+ getRequestHeaders,
+ setResponseHeader,
+} from '@tanstack/vue-start/server'
+import type { RequestHeaderName } from '@tanstack/vue-start/server'
+import { defineComponent, ref } from 'vue'
+
+export const getTestHeaders = createServerFn().handler(() => {
+ setResponseHeader('x-test-header', 'test-value')
+ const reqHeaders = Object.fromEntries(getRequestHeaders().entries())
+
+ return {
+ serverHeaders: reqHeaders,
+ headers: reqHeaders,
+ }
+})
+
+type TestHeadersResult = {
+ headers?: Partial>
+ serverHeaders?: Partial>
+}
+
+const HeadersRouteComponent = defineComponent({
+ setup() {
+ const loaderData = Route.useLoaderData()
+ const testHeadersResult = ref(null)
+
+ return () => (
+
+
Headers Test
+
+
+
Initial Headers:
+
+ {JSON.stringify(loaderData.value.testHeaders.headers, null, 2)}
+
+ {testHeadersResult.value && (
+ <>
+
Updated Headers:
+
+ {JSON.stringify(testHeadersResult.value.headers, null, 2)}
+
+ >
+ )}
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/headers')({
+ loader: async () => {
+ return {
+ testHeaders: await getTestHeaders(),
+ }
+ },
+ component: HeadersRouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/index.tsx b/e2e/vue-start/server-functions/src/routes/index.tsx
new file mode 100644
index 00000000000..155fb3dff1a
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/index.tsx
@@ -0,0 +1,78 @@
+import { Link, createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/')({
+ component: Home,
+})
+
+function Home() {
+ return (
+
+
Server functions E2E tests
+
+
+
+ Consistent server function returns both on client and server for GET
+ and POST calls
+
+
+
+
+ submitting multipart/form-data as server function input
+
+
+
+
+ Server function can return null for GET and POST calls
+
+
+
+
+ Server function can correctly send and receive FormData
+
+
+
+
+ server function can correctly send and receive headers
+
+
+
+
+ Direct POST submitting FormData to a Server function returns the
+ correct message
+
+
+
+
+ invoking a server function with custom response status code
+
+
+
+
+ isomorphic functions can have different implementations on client
+ and server
+
+
+
+
+ env-only functions can only be called on the server or client
+ respectively
+
+
+
+ server function sets cookies
+
+
+
+ dead code elimation only affects code after transformation
+
+
+
+ aborting a server function call
+
+
+ server function returns raw response
+
+
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx b/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx
new file mode 100644
index 00000000000..6d0679e34dc
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx
@@ -0,0 +1,82 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createIsomorphicFn, createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const getEnv = createIsomorphicFn()
+ .server(() => 'server')
+ .client(() => 'client')
+
+const getServerEnv = createServerFn().handler(() => getEnv())
+
+const getEcho = createIsomorphicFn()
+ .server((input: string) => 'server received ' + input)
+ .client((input) => 'client received ' + input)
+
+const getServerEcho = createServerFn()
+ .inputValidator((input: string) => input)
+ .handler(({ data }) => getEcho(data))
+
+const RouteComponent = defineComponent({
+ setup() {
+ const loaderData = Route.useLoaderData()
+ const results = ref> | undefined>()
+
+ async function handleClick() {
+ const envOnClick = getEnv()
+ const echo = getEcho('hello')
+ const [serverEnv, serverEcho] = await Promise.all([
+ getServerEnv(),
+ getServerEcho({ data: 'hello' }),
+ ])
+ results.value = { envOnClick, echo, serverEnv, serverEcho }
+ }
+
+ return () => (
+
+
+ Run
+
+ {!!results.value && (
+
+
+ getEnv
+
+ When we called the function on the server it returned:
+
+ {JSON.stringify(results.value.serverEnv)}
+
+ When we called the function on the client it returned:
+
+ {JSON.stringify(results.value.envOnClick)}
+
+ When we called the function during SSR it returned:
+
+ {JSON.stringify(loaderData.value.envOnLoad)}
+
+
+
+ echo
+
+ When we called the function on the server it returned:
+
+ {JSON.stringify(results.value.serverEcho)}
+
+ When we called the function on the client it returned:
+
+ {JSON.stringify(results.value.echo)}
+
+
+ )}
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/isomorphic-fns')({
+ component: RouteComponent,
+ loader() {
+ return {
+ envOnLoad: getEnv(),
+ }
+ },
+})
diff --git a/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx b/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx
new file mode 100644
index 00000000000..b13fb972fa4
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx
@@ -0,0 +1,83 @@
+import { createFileRoute, useRouter } from '@tanstack/vue-router'
+import {
+ createMiddleware,
+ createServerFn,
+ getRouterInstance,
+} from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const middleware = createMiddleware({ type: 'function' }).client(
+ async ({ next }) => {
+ const router = await getRouterInstance()
+ return next({
+ sendContext: {
+ routerContext: router.options.context,
+ },
+ })
+ },
+)
+
+const serverFn = createServerFn()
+ .middleware([middleware])
+ .handler(({ context }) => {
+ return context.routerContext
+ })
+const RouteComponent = defineComponent({
+ setup() {
+ const serverFnClientResult = ref({})
+ const loaderData = Route.useLoaderData()
+ const router = useRouter()
+
+ return () => (
+
+
Client Middleware has access to router instance
+
+ This component checks that the client middleware has access to the
+ router instance and thus its context.
+
+
+ It should return{' '}
+
+
+ {JSON.stringify(router.options.context)}
+
+
+
+
+ serverFn when invoked in the loader returns:
+
+
+ {JSON.stringify(serverFnClientResult.value)}
+
+
+
+ serverFn when invoked on the client returns:
+
+
+ {JSON.stringify(loaderData.value.serverFnLoaderResult)}
+
+
+
{
+ serverFn().then((data) => {
+ serverFnClientResult.value = data
+ })
+ }}
+ >
+ Invoke Server Function
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/middleware/client-middleware-router')({
+ component: RouteComponent,
+ loader: async () => ({ serverFnLoaderResult: await serverFn() }),
+})
diff --git a/e2e/vue-start/server-functions/src/routes/middleware/index.tsx b/e2e/vue-start/server-functions/src/routes/middleware/index.tsx
new file mode 100644
index 00000000000..45c68fd1b12
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/middleware/index.tsx
@@ -0,0 +1,37 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/middleware/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Server functions middleware E2E tests
+
+
+
+ Client Middleware has access to router instance
+
+
+
+
+ Client Middleware can send server function reference in context
+
+
+
+
+ Request Middleware in combination with server function
+
+
+
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx b/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx
new file mode 100644
index 00000000000..63c0fe2b748
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx
@@ -0,0 +1,84 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createMiddleware, createServerFn } from '@tanstack/vue-start'
+import { getRequest } from '@tanstack/vue-start/server'
+import { defineComponent, ref } from 'vue'
+
+const requestMiddleware = createMiddleware({ type: 'request' }).server(
+ async ({ next, request }) => {
+ return next({
+ context: {
+ requestParam: request.url,
+ requestFunc: getRequest().url,
+ },
+ })
+ },
+)
+
+const serverFn = createServerFn()
+ .middleware([requestMiddleware])
+ .handler(async ({ context: { requestParam, requestFunc } }) => {
+ return { requestParam, requestFunc }
+ })
+
+type ServerFnResult = Awaited>
+
+const RouteComponent = defineComponent({
+ setup() {
+ const loaderData = Route.useLoaderData()
+ const clientData = ref(null)
+
+ return () => (
+
+
Request Middleware in combination with server function
+
+
+
+
Loader Data Request Param:
+
+ {loaderData.value.requestParam}
+
+ Request Func:
+
+ {loaderData.value.requestFunc}
+
+
+
+
+ {
+ const data = await serverFn()
+ clientData.value = data
+ }}
+ >
+ Call server function from client
+
+
+
+
+
Client Data
+ {clientData.value ? (
+
+ Request Param:
+
+ {clientData.value.requestParam}
+
+ Request Func:
+
+ {clientData.value.requestFunc}
+
+
+ ) : (
+ ' Loading ...'
+ )}
+
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/middleware/request-middleware')({
+ loader: () => serverFn(),
+ component: RouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx b/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx
new file mode 100644
index 00000000000..ff6ef46efb4
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx
@@ -0,0 +1,82 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createMiddleware, createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const middleware = createMiddleware({ type: 'function' }).client(
+ async ({ next }) => {
+ return next({
+ sendContext: {
+ serverFn: barFn,
+ },
+ })
+ },
+)
+
+const fooFn = createServerFn()
+ .middleware([middleware])
+ .handler(({ context }) => {
+ return context.serverFn()
+ })
+const barFn = createServerFn().handler(() => {
+ return 'bar'
+})
+
+const RouteComponent = defineComponent({
+ setup() {
+ const serverFnClientResult = ref({})
+ const loaderData = Route.useLoaderData()
+
+ return () => (
+
+
Send server function in context
+
+ This component checks that the client middleware can send a reference
+ to a server function in the context, which can then be invoked in the
+ server function handler.
+
+
+ It should return{' '}
+
+
+ {JSON.stringify('bar')}
+
+
+
+
+ serverFn when invoked in the loader returns:
+
+
+ {JSON.stringify(serverFnClientResult.value)}
+
+
+
+ serverFn when invoked on the client returns:
+
+
+ {JSON.stringify(loaderData.value.serverFnLoaderResult)}
+
+
+
{
+ fooFn().then((data) => {
+ serverFnClientResult.value = data
+ })
+ }}
+ >
+ Invoke Server Function
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/middleware/send-serverFn')({
+ component: RouteComponent,
+ loader: async () => ({ serverFnLoaderResult: await fooFn() }),
+})
diff --git a/e2e/vue-start/server-functions/src/routes/multipart.tsx b/e2e/vue-start/server-functions/src/routes/multipart.tsx
new file mode 100644
index 00000000000..c41fd8e4e58
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/multipart.tsx
@@ -0,0 +1,113 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const multipartFormDataServerFn = createServerFn({ method: 'POST' })
+ .inputValidator((x: unknown) => {
+ if (!(x instanceof FormData)) {
+ throw new Error('Invalid form data')
+ }
+
+ const value = x.get('input_field')
+ const file = x.get('input_file')
+
+ if (typeof value !== 'string') {
+ throw new Error('Submitted value is not a string')
+ }
+
+ if (!(file instanceof File)) {
+ throw new Error('File is required')
+ }
+
+ return {
+ submittedValue: value,
+ file,
+ }
+ })
+ .handler(async ({ data }) => {
+ const contents = await data.file.text()
+ return {
+ value: data.submittedValue,
+ file: {
+ name: data.file.name,
+ size: data.file.size,
+ contents: contents,
+ },
+ }
+ })
+
+const MultipartServerFnCall = defineComponent({
+ setup() {
+ const formRef = ref(null)
+ const multipartResult = ref({})
+
+ const handleSubmit = (e: Event) => {
+ e.preventDefault()
+
+ if (!formRef.value) {
+ return
+ }
+
+ const formData = new FormData(formRef.value)
+ multipartFormDataServerFn({ data: formData }).then((data) => {
+ multipartResult.value = data
+ })
+ }
+
+ return () => (
+
+
Multipart Server Fn POST Call
+
+ It should return{' '}
+
+
+ {JSON.stringify({
+ value: 'test field value',
+ file: { name: 'my_file.txt', size: 9, contents: 'test data' },
+ })}
+
+
+
+
+
+
+ {JSON.stringify(multipartResult.value)}
+
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/multipart')({
+ component: MultipartServerFnCall,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/primitives/index.tsx b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx
new file mode 100644
index 00000000000..561f7646eb4
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx
@@ -0,0 +1,146 @@
+import { useQuery } from '@tanstack/vue-query'
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { defineComponent } from 'vue'
+import { z } from 'zod'
+
+function stringify(data: any) {
+ return JSON.stringify(data === undefined ? '$undefined' : data)
+}
+
+const $stringPost = createServerFn({ method: 'POST' })
+ .inputValidator(z.string())
+ .handler((ctx) => ctx.data)
+
+const $stringGet = createServerFn({ method: 'GET' })
+ .inputValidator(z.string())
+ .handler((ctx) => ctx.data)
+
+const $undefinedPost = createServerFn({ method: 'POST' })
+ .inputValidator(z.undefined())
+ .handler((ctx) => ctx.data)
+
+const $undefinedGet = createServerFn({ method: 'GET' })
+ .inputValidator(z.undefined())
+ .handler((ctx) => ctx.data)
+
+const $nullPost = createServerFn({ method: 'POST' })
+ .inputValidator(z.null())
+ .handler((ctx) => ctx.data)
+
+const $nullGet = createServerFn({ method: 'GET' })
+ .inputValidator(z.null())
+ .handler((ctx) => ctx.data)
+
+interface PrimitiveComponentProps {
+ serverFn: {
+ get: (opts: { data: T }) => Promise
+ post: (opts: { data: T }) => Promise
+ }
+ data: {
+ value: T
+ type: string
+ }
+}
+
+function makeTestCase(props: PrimitiveComponentProps) {
+ return props
+}
+const testCases = [
+ makeTestCase({
+ data: {
+ value: null,
+ type: 'null',
+ },
+ serverFn: {
+ get: $nullGet,
+ post: $nullPost,
+ },
+ }),
+ makeTestCase({
+ data: {
+ value: undefined,
+ type: 'undefined',
+ },
+ serverFn: {
+ get: $undefinedGet,
+ post: $undefinedPost,
+ },
+ }),
+ makeTestCase({
+ data: {
+ value: 'foo-bar',
+ type: 'string',
+ },
+ serverFn: {
+ get: $stringGet,
+ post: $stringPost,
+ },
+ }),
+] as Array>
+
+type Method = 'get' | 'post'
+
+const RouteComponent = defineComponent({
+ setup() {
+ const testQueries = testCases.map((testCase) => {
+ const makeQuery = (method: Method) =>
+ useQuery(() => ({
+ queryKey: [testCase.data.type, method],
+ queryFn: async () => {
+ const result = await testCase.serverFn[method]({
+ data: testCase.data.value,
+ })
+ if (result === undefined) {
+ return '$undefined'
+ }
+ return result
+ },
+ }))
+
+ return {
+ testCase,
+ queries: {
+ post: makeQuery('post'),
+ get: makeQuery('get'),
+ },
+ }
+ })
+
+ return () => (
+ <>
+ {testQueries.map(({ testCase, queries }) => (
+
+
data type: {testCase.data.type}
+
+ {(['post', 'get'] as const).map((method) => {
+ const testId = `${method}-${testCase.data.type}`
+ const query = queries[method]
+ return (
+
+
serverFn method={method}
+
expected
+
+ {stringify(testCase.data.value)}
+
+
result
+
+ {query.isSuccess.value ? stringify(query.data.value) : ''}
+
+
+
+ )
+ })}
+
+
+
+ ))}
+ >
+ )
+ },
+})
+
+export const Route = createFileRoute('/primitives/')({
+ component: RouteComponent,
+ ssr: true,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/raw-response.tsx b/e2e/vue-start/server-functions/src/routes/raw-response.tsx
new file mode 100644
index 00000000000..7c3b87d8633
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/raw-response.tsx
@@ -0,0 +1,50 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const expectedValue = 'Hello from a server function!'
+export const rawResponseFn = createServerFn().handler(() => {
+ return new Response(expectedValue)
+})
+
+const RouteComponent = defineComponent({
+ setup() {
+ const formDataResult = ref('')
+
+ return () => (
+
+
Raw Response
+
+ It should return{' '}
+
+ {expectedValue}
+
+
+
+
{
+ const response = await rawResponseFn()
+ console.log('response', response)
+
+ const text = await response.text()
+ formDataResult.value = text
+ }}
+ data-testid="button"
+ class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
+ >
+ Submit
+
+
+
+
+ {JSON.stringify(formDataResult.value)}
+
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/raw-response')({
+ component: RouteComponent,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx
new file mode 100644
index 00000000000..46d6efcefd4
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx
@@ -0,0 +1,38 @@
+import { useQuery } from '@tanstack/vue-query'
+import { createFileRoute, redirect } from '@tanstack/vue-router'
+import { createServerFn, useServerFn } from '@tanstack/vue-start'
+import { Suspense, defineComponent } from 'vue'
+
+const $redirectServerFn = createServerFn({ method: 'GET' }).handler(
+ async () => {
+ throw redirect({ to: '/redirect-test-ssr/target' })
+ },
+)
+
+const RouteComponent = defineComponent({
+ setup() {
+ const redirectFn = useServerFn($redirectServerFn)
+ const query = useQuery(() => ({
+ queryKey: ['redirect-test-ssr'],
+ queryFn: () => redirectFn(),
+ suspense: true,
+ }))
+
+ return () => (
+
+
Redirect Source SSR
+
+ {{
+ default: () => {JSON.stringify(query.data.value)}
,
+ fallback: () => Loading...
,
+ }}
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/redirect-test-ssr/')({
+ component: RouteComponent,
+ ssr: true,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/target.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/target.tsx
new file mode 100644
index 00000000000..4f9584232ec
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/target.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/redirect-test-ssr/target')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Redirect Target SSR
+
Successfully redirected!
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx
new file mode 100644
index 00000000000..87df7fa5b5e
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx
@@ -0,0 +1,38 @@
+import { useQuery } from '@tanstack/vue-query'
+import { createFileRoute, redirect } from '@tanstack/vue-router'
+import { createServerFn, useServerFn } from '@tanstack/vue-start'
+import { Suspense, defineComponent } from 'vue'
+
+const $redirectServerFn = createServerFn({ method: 'GET' }).handler(
+ async () => {
+ throw redirect({ to: '/redirect-test/target' })
+ },
+)
+
+const RouteComponent = defineComponent({
+ setup() {
+ const redirectFn = useServerFn($redirectServerFn)
+ const query = useQuery(() => ({
+ queryKey: ['redirect-test'],
+ queryFn: () => redirectFn(),
+ suspense: true,
+ }))
+
+ return () => (
+
+
Redirect Source
+
+ {{
+ default: () => {JSON.stringify(query.data.value)}
,
+ fallback: () => Loading...
,
+ }}
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/redirect-test/')({
+ component: RouteComponent,
+ ssr: 'data-only',
+})
diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test/target.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test/target.tsx
new file mode 100644
index 00000000000..10b25f0a153
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/redirect-test/target.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/vue-router'
+
+export const Route = createFileRoute('/redirect-test/target')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
Redirect Target
+
Successfully redirected!
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/return-null.tsx b/e2e/vue-start/server-functions/src/routes/return-null.tsx
new file mode 100644
index 00000000000..de88ad71138
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/return-null.tsx
@@ -0,0 +1,74 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+/**
+ * This checks whether the server function can
+ * return null without throwing an error or returning something else.
+ * @link https://github.com/TanStack/router/issues/2776
+ */
+
+const $allow_return_null_getFn = createServerFn().handler(async () => {
+ return null
+})
+const $allow_return_null_postFn = createServerFn({ method: 'POST' }).handler(
+ async () => {
+ return null
+ },
+)
+
+const AllowServerFnReturnNull = defineComponent({
+ setup() {
+ const getServerResult = ref('-')
+ const postServerResult = ref('-')
+
+ return () => (
+
+
Allow ServerFn to return `null`
+
+ This component checks whether the server function can return null
+ without throwing an error.
+
+
+ It should return{' '}
+
+ {JSON.stringify(null)}
+
+
+
+ {`GET: $allow_return_null_getFn returns`}
+
+
+ {JSON.stringify(getServerResult.value)}
+
+
+
+ {`POST: $allow_return_null_postFn returns`}
+
+
+ {JSON.stringify(postServerResult.value)}
+
+
+
{
+ $allow_return_null_getFn().then((data) => {
+ getServerResult.value = data
+ })
+ $allow_return_null_postFn().then((data) => {
+ postServerResult.value = data
+ })
+ }}
+ >
+ Test Allow Server Fn Return Null
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/return-null')({
+ component: AllowServerFnReturnNull,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx
new file mode 100644
index 00000000000..e2b117a9ad1
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx
@@ -0,0 +1,88 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+import { defineComponent, ref } from 'vue'
+
+const testValues = {
+ name: 'Sean',
+ age: 25,
+ pet1: 'dog',
+ pet2: 'cat',
+ __adder: 1,
+}
+
+export const greetUser = createServerFn({ method: 'POST' })
+ .inputValidator((data: FormData) => {
+ if (!(data instanceof FormData)) {
+ throw new Error('Invalid! FormData is required')
+ }
+ const name = data.get('name')
+ const age = data.get('age')
+ const pets = data.getAll('pet')
+
+ if (!name || !age || pets.length === 0) {
+ throw new Error('Name, age and pets are required')
+ }
+
+ return {
+ name: name.toString(),
+ age: parseInt(age.toString(), 10),
+ pets: pets.map((pet) => pet.toString()),
+ }
+ })
+ .handler(({ data: { name, age, pets } }) => {
+ return `Hello, ${name}! You are ${age + testValues.__adder} years old, and your favorite pets are ${pets.join(',')}.`
+ })
+
+export const SerializeFormDataFnCall = defineComponent({
+ setup() {
+ const formDataResult = ref('')
+
+ return () => (
+
+
Serialize FormData Fn POST Call
+
+ It should return{' '}
+
+
+ Hello, {testValues.name}! You are{' '}
+ {testValues.age + testValues.__adder} years old, and your favorite
+ pets are {testValues.pet1},{testValues.pet2}.
+
+
+
+
+
+
+ {JSON.stringify(formDataResult.value)}
+
+
+
+ )
+ },
+})
+
+export const Route = createFileRoute('/serialize-form-data')({
+ component: SerializeFormDataFnCall,
+})
diff --git a/e2e/vue-start/server-functions/src/routes/status.tsx b/e2e/vue-start/server-functions/src/routes/status.tsx
new file mode 100644
index 00000000000..ba627bd3715
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/status.tsx
@@ -0,0 +1,30 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn, useServerFn } from '@tanstack/vue-start'
+import { setResponseStatus } from '@tanstack/vue-start/server'
+
+const helloFn = createServerFn().handler(() => {
+ setResponseStatus(225, `hello`)
+ return {
+ hello: 'world',
+ }
+})
+
+export const Route = createFileRoute('/status')({
+ component: StatusComponent,
+})
+
+function StatusComponent() {
+ const hello = useServerFn(helloFn)
+
+ return (
+
+ hello()}
+ >
+ click me
+
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/routes/submit-post-formdata.tsx b/e2e/vue-start/server-functions/src/routes/submit-post-formdata.tsx
new file mode 100644
index 00000000000..35e083326d7
--- /dev/null
+++ b/e2e/vue-start/server-functions/src/routes/submit-post-formdata.tsx
@@ -0,0 +1,60 @@
+import { createFileRoute } from '@tanstack/vue-router'
+import { createServerFn } from '@tanstack/vue-start'
+
+export const Route = createFileRoute('/submit-post-formdata')({
+ component: SubmitPostFormDataFn,
+})
+
+const testValues = {
+ name: 'Sean',
+}
+
+export const greetUser = createServerFn({ method: 'POST' })
+ .inputValidator((data: FormData) => {
+ if (!(data instanceof FormData)) {
+ throw new Error('Invalid! FormData is required')
+ }
+ const name = data.get('name')
+
+ if (!name) {
+ throw new Error('Name is required')
+ }
+
+ return {
+ name: name.toString(),
+ }
+ })
+ .handler(({ data: { name } }) => {
+ return new Response(`Hello, ${name}!`)
+ })
+
+function SubmitPostFormDataFn() {
+ return (
+
+
Submit POST FormData Fn Call
+
+ It should return navigate and return{' '}
+
+
+ Hello, {testValues.name}!
+
+
+
+
+
+ )
+}
diff --git a/e2e/vue-start/server-functions/src/styles/app.css b/e2e/vue-start/server-functions/src/styles/app.css
new file mode 100644
index 00000000000..c36c737cd46
--- /dev/null
+++ b/e2e/vue-start/server-functions/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-functions/src/vite-env.d.ts b/e2e/vue-start/server-functions/src/vite-env.d.ts
new file mode 100644
index 00000000000..0b2af560d60
--- /dev/null
+++ b/e2e/vue-start/server-functions/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-functions/tests/server-functions.spec.ts b/e2e/vue-start/server-functions/tests/server-functions.spec.ts
new file mode 100644
index 00000000000..3b32deb5bdd
--- /dev/null
+++ b/e2e/vue-start/server-functions/tests/server-functions.spec.ts
@@ -0,0 +1,497 @@
+import * as fs from 'node:fs'
+import { expect } from '@playwright/test'
+import { test } from '@tanstack/router-e2e-utils'
+import { PORT } from '../playwright.config'
+import type { Page } from '@playwright/test'
+
+test('Server function URLs correctly include constant ids', async ({
+ page,
+}) => {
+ for (const currentPage of ['/submit-post-formdata', '/formdata-redirect']) {
+ await page.goto(currentPage)
+ await page.waitForLoadState('networkidle')
+
+ const form = page.locator('form')
+ const actionUrl = await form.getAttribute('action')
+
+ expect(actionUrl).toMatch(/^\/_serverFn\/constant_id/)
+ }
+})
+
+test('invoking a server function with custom response status code', async ({
+ page,
+}) => {
+ await page.goto('/status')
+
+ await page.waitForLoadState('networkidle')
+
+ const requestPromise = new Promise((resolve) => {
+ page.on('response', (response) => {
+ expect(response.status()).toBe(225)
+ expect(response.statusText()).toBe('hello')
+ expect(response.headers()['content-type']).toContain('application/json')
+ resolve()
+ })
+ })
+ await page.getByTestId('invoke-server-fn').click()
+ await requestPromise
+})
+
+test('Consistent server function returns both on client and server for GET and POST calls', async ({
+ page,
+}) => {
+ await page.goto('/consistent')
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-consistent-server-fns-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-consistent-server-fn-calls-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ // GET calls
+ await expect(page.getByTestId('cons_serverGetFn1-response')).toContainText(
+ expected,
+ )
+ await expect(page.getByTestId('cons_getFn1-response')).toContainText(expected)
+
+ // POST calls
+ await expect(page.getByTestId('cons_serverPostFn1-response')).toContainText(
+ expected,
+ )
+ await expect(page.getByTestId('cons_postFn1-response')).toContainText(
+ expected,
+ )
+})
+
+test('submitting multipart/form-data as server function input', async ({
+ page,
+}) => {
+ await page.goto('/multipart')
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-multipart-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ const fileChooserPromise = page.waitForEvent('filechooser')
+ await page.getByTestId('multipart-form-file-input').click()
+ const fileChooser = await fileChooserPromise
+ await fileChooser.setFiles({
+ name: 'my_file.txt',
+ mimeType: 'text/plain',
+ buffer: Buffer.from('test data', 'utf-8'),
+ })
+ await page.getByText('Submit (onClick)').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('multipart-form-response')).toContainText(
+ expected,
+ )
+})
+
+test('isomorphic functions can have different implementations on client and server', async ({
+ page,
+}) => {
+ await page.goto('/isomorphic-fns')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('test-isomorphic-results-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('server-result')).toContainText('server')
+ await expect(page.getByTestId('client-result')).toContainText('client')
+ await expect(page.getByTestId('ssr-result')).toContainText('server')
+
+ await expect(page.getByTestId('server-echo-result')).toContainText(
+ 'server received hello',
+ )
+ await expect(page.getByTestId('client-echo-result')).toContainText(
+ 'client received hello',
+ )
+})
+
+test('env-only functions can only be called on the server or client respectively', async ({
+ page,
+}) => {
+ await page.goto('/env-only')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('test-env-only-results-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('server-on-server')).toContainText(
+ 'server got: hello',
+ )
+ await expect(page.getByTestId('server-on-client')).toContainText(
+ 'serverEcho threw an error: createServerOnlyFn() functions can only be called on the server!',
+ )
+
+ await expect(page.getByTestId('client-on-server')).toContainText(
+ 'clientEcho threw an error: createClientOnlyFn() functions can only be called on the client!',
+ )
+ await expect(page.getByTestId('client-on-client')).toContainText(
+ 'client got: hello',
+ )
+})
+
+test('Server function can return null for GET and POST calls', async ({
+ page,
+}) => {
+ await page.goto('/return-null')
+
+ await page.waitForLoadState('networkidle')
+ await page.getByTestId('test-allow-server-fn-return-null-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ // GET call
+ await expect(
+ page.getByTestId('allow_return_null_getFn-response'),
+ ).toContainText(JSON.stringify(null))
+
+ // POST call
+ await expect(
+ page.getByTestId('allow_return_null_postFn-response'),
+ ).toContainText(JSON.stringify(null))
+})
+
+test('Server function can correctly send and receive FormData', async ({
+ page,
+}) => {
+ await page.goto('/serialize-form-data')
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-serialize-formdata-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-serialize-formdata-fn-calls-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(
+ page.getByTestId('serialize-formdata-form-response'),
+ ).toContainText(expected)
+})
+
+test('server function can correctly send and receive headers', async ({
+ page,
+}) => {
+ await page.goto('/headers')
+
+ await page.waitForLoadState('networkidle')
+ let headers = JSON.parse(
+ await page.getByTestId('initial-headers-result').innerText(),
+ )
+ expect(headers['host']).toBe(`localhost:${PORT}`)
+ expect(headers['user-agent']).toContain('Mozilla/5.0')
+ expect(headers['sec-fetch-mode']).toBe('navigate')
+
+ await page.getByTestId('test-headers-btn').click()
+ await page.waitForSelector('[data-testid="updated-headers-result"]')
+
+ headers = JSON.parse(
+ await page.getByTestId('updated-headers-result').innerText(),
+ )
+
+ expect(headers['host']).toBe(`localhost:${PORT}`)
+ expect(headers['user-agent']).toContain('Mozilla/5.0')
+ expect(headers['sec-fetch-mode']).toBe('cors')
+ expect(headers['referer']).toBe(`http://localhost:${PORT}/headers`)
+})
+
+test('Direct POST submitting FormData to a Server function returns the correct message', async ({
+ page,
+}) => {
+ await page.goto('/submit-post-formdata')
+
+ await page.waitForLoadState('networkidle')
+
+ const expected =
+ (await page
+ .getByTestId('expected-submit-post-formdata-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ const result = await page.innerText('body')
+ expect(result).toBe(expected)
+})
+
+test("server function's dead code is preserved if already there", async ({
+ page,
+}) => {
+ await page.goto('/dead-code-preserve')
+
+ await page.waitForLoadState('networkidle')
+ await page.getByTestId('test-dead-code-fn-call-btn').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('dead-code-fn-call-response')).toContainText(
+ '1',
+ )
+
+ await fs.promises.rm('count-effect.txt')
+})
+
+test.describe('server function sets cookies', () => {
+ async function runCookieTest(page: Page, expectedCookieValue: string) {
+ for (let i = 1; i <= 4; i++) {
+ const key = `cookie-${i}-${expectedCookieValue}`
+
+ const actualValue = await page.getByTestId(key).textContent()
+ expect(actualValue).toBe(expectedCookieValue)
+ }
+ }
+ test('SSR', async ({ page }) => {
+ const expectedCookieValue = `SSR-${Date.now()}`
+ await page.goto(`/cookies/set?value=${expectedCookieValue}`)
+ await runCookieTest(page, expectedCookieValue)
+ })
+
+ test('client side navigation', async ({ page }) => {
+ const expectedCookieValue = `CLIENT-${Date.now()}`
+ await page.goto(`/cookies?value=${expectedCookieValue}`)
+ await page.getByTestId('link-to-set').click()
+ await runCookieTest(page, expectedCookieValue)
+ })
+})
+
+test.describe('aborting a server function call', () => {
+ test('without aborting', async ({ page }) => {
+ await page.goto('/abort-signal')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('run-without-abort-btn').click()
+ await page.waitForLoadState('networkidle')
+ await page.waitForSelector(
+ '[data-testid="result"]:has-text("server function result")',
+ )
+ await page.waitForSelector(
+ '[data-testid="errorMessage"]:has-text("$undefined")',
+ )
+
+ const result = (await page.getByTestId('result').textContent()) || ''
+ expect(result).toBe('server function result')
+
+ const errorMessage =
+ (await page.getByTestId('errorMessage').textContent()) || ''
+ expect(errorMessage).toBe('$undefined')
+ })
+
+ test('aborting', async ({ page }) => {
+ await page.goto('/abort-signal')
+
+ await page.waitForLoadState('networkidle')
+
+ await page.getByTestId('run-with-abort-btn').click()
+ await page.waitForLoadState('networkidle')
+ await page.waitForSelector('[data-testid="result"]:has-text("$undefined")')
+ await page.waitForSelector(
+ '[data-testid="errorMessage"]:has-text("aborted")',
+ )
+
+ const result = (await page.getByTestId('result').textContent()) || ''
+ expect(result).toBe('$undefined')
+
+ const errorMessage =
+ (await page.getByTestId('errorMessage').textContent()) || ''
+ expect(errorMessage).toContain('abort')
+ })
+})
+
+test('raw response', async ({ page }) => {
+ await page.goto('/raw-response')
+
+ await page.waitForLoadState('networkidle')
+
+ const expectedValue = (await page.getByTestId('expected').textContent()) || ''
+ expect(expectedValue).not.toBe('')
+
+ await page.getByTestId('button').click()
+ await page.waitForLoadState('networkidle')
+
+ await expect(page.getByTestId('response')).toContainText(expectedValue)
+})
+
+test.describe('formdata redirect modes', () => {
+ for (const mode of ['js', 'no-js']) {
+ test(`Server function can redirect when sending formdata: mode = ${mode}`, async ({
+ page,
+ }) => {
+ await page.goto('/formdata-redirect?mode=' + mode)
+
+ await page.waitForLoadState('networkidle')
+ const expected =
+ (await page
+ .getByTestId('expected-submit-post-formdata-server-fn-result')
+ .textContent()) || ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click()
+
+ await page.waitForLoadState('networkidle')
+
+ await expect(
+ page.getByTestId('formdata-redirect-target-name'),
+ ).toContainText(expected)
+
+ expect(page.url().endsWith(`/formdata-redirect/target/${expected}`))
+ })
+ }
+})
+
+test.describe('middleware', () => {
+ test.describe('client middleware should have access to router context via the router instance', () => {
+ async function runTest(page: Page) {
+ await page.waitForLoadState('networkidle')
+
+ const expected =
+ (await page.getByTestId('expected-server-fn-result').textContent()) ||
+ ''
+ expect(expected).not.toBe('')
+
+ await page.getByTestId('btn-serverFn').click()
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('serverFn-loader-result')).toContainText(
+ expected,
+ )
+ await expect(page.getByTestId('serverFn-client-result')).toContainText(
+ expected,
+ )
+ }
+
+ test('direct visit', async ({ page }) => {
+ await page.goto('/middleware/client-middleware-router')
+ await runTest(page)
+ })
+
+ test('client navigation', async ({ page }) => {
+ await page.goto('/middleware')
+ await page.getByTestId('client-middleware-router-link').click()
+ await runTest(page)
+ })
+ })
+
+ test('server function in combination with request middleware', async ({
+ page,
+ }) => {
+ await page.goto('/middleware/request-middleware')
+
+ await page.waitForLoadState('networkidle')
+
+ async function checkEqual(prefix: string) {
+ const requestParam = await page
+ .getByTestId(`${prefix}-data-request-param`)
+ .textContent()
+ expect(requestParam).not.toBe('')
+ const requestFunc = await page
+ .getByTestId(`${prefix}-data-request-func`)
+ .textContent()
+ expect(requestParam).toBe(requestFunc)
+ }
+
+ await checkEqual('loader')
+
+ await page.getByTestId('client-call-button').click()
+ await page.waitForLoadState('networkidle')
+
+ await checkEqual('client')
+ })
+})
+
+test('factory', async ({ page }) => {
+ await page.goto('/factory')
+
+ await expect(page.getByTestId('factory-route-component')).toBeInViewport()
+
+ const buttons = await page
+ .locator('[data-testid^="btn-fn-"]')
+ .elementHandles()
+ for (const button of buttons) {
+ const testId = await button.getAttribute('data-testid')
+
+ if (!testId) {
+ throw new Error('Button is missing data-testid')
+ }
+
+ const suffix = testId.replace('btn-fn-', '')
+
+ const expected =
+ (await page.getByTestId(`expected-fn-result-${suffix}`).textContent()) ||
+ ''
+ expect(expected).not.toBe('')
+
+ await button.click()
+
+ await expect(page.getByTestId(`fn-result-${suffix}`)).toContainText(
+ expected,
+ )
+
+ await expect(page.getByTestId(`fn-comparison-${suffix}`)).toContainText(
+ 'equal',
+ )
+ }
+})
+
+test('primitives', async ({ page }) => {
+ await page.goto('/primitives')
+
+ await page.waitForLoadState('networkidle')
+
+ // Wait for client-side hydration to complete
+ await expect(page.locator('[data-testid^="expected-"]').first()).toBeVisible()
+
+ 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)
+ }
+})
+
+test('redirect in server function on direct navigation', async ({ page }) => {
+ // Test direct navigation to a route with a server function that redirects
+ await page.goto('/redirect-test')
+
+ // Should redirect to target page
+ await expect(page.getByTestId('redirect-target')).toBeVisible()
+ expect(page.url()).toContain('/redirect-test/target')
+})
+
+test('redirect in server function called in query during SSR', async ({
+ page,
+}) => {
+ // Test direct navigation to a route with a server function that redirects
+ // when called inside a query with ssr: true
+ await page.goto('/redirect-test-ssr')
+
+ // Should redirect to target page
+ await expect(page.getByTestId('redirect-target-ssr')).toBeVisible()
+ expect(page.url()).toContain('/redirect-test-ssr/target')
+})
diff --git a/e2e/vue-start/server-functions/tsconfig.json b/e2e/vue-start/server-functions/tsconfig.json
new file mode 100644
index 00000000000..a5ae5ae7e47
--- /dev/null
+++ b/e2e/vue-start/server-functions/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-functions/vite.config.ts b/e2e/vue-start/server-functions/vite.config.ts
new file mode 100644
index 00000000000..4c4af226a34
--- /dev/null
+++ b/e2e/vue-start/server-functions/vite.config.ts
@@ -0,0 +1,32 @@
+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'
+
+const FUNCTIONS_WITH_CONSTANT_ID = [
+ 'src/routes/submit-post-formdata.tsx/greetUser_createServerFn_handler',
+ 'src/routes/formdata-redirect/index.tsx/greetUser_createServerFn_handler',
+]
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ plugins: [
+ tsConfigPaths({
+ projects: ['./tsconfig.json'],
+ }),
+ tanstackStart({
+ serverFns: {
+ generateFunctionId: (opts) => {
+ const id = `${opts.filename}/${opts.functionName}`
+ if (FUNCTIONS_WITH_CONSTANT_ID.includes(id)) return 'constant_id'
+ else return undefined
+ },
+ },
+ }),
+ vue(),
+ vueJsx(),
+ ],
+})
diff --git a/e2e/vue-start/server-routes/package.json b/e2e/vue-start/server-routes/package.json
index 31a1444bccb..680edbd581b 100644
--- a/e2e/vue-start/server-routes/package.json
+++ b/e2e/vue-start/server-routes/package.json
@@ -32,7 +32,7 @@
"@types/node": "^22.10.2",
"combinate": "^1.1.11",
"postcss": "^8.5.1",
- "srvx": "^0.8.6",
+ "srvx": "^0.9.8",
"tailwindcss": "^4.1.17",
"typescript": "^5.7.2",
"@vitejs/plugin-vue": "^6.0.3",
diff --git a/packages/vue-router/README.md b/packages/vue-router/README.md
index aef2489ce02..25c561a896e 100644
--- a/packages/vue-router/README.md
+++ b/packages/vue-router/README.md
@@ -2,7 +2,7 @@
# TanStack Vue Router
-
+
🤖 Type-safe router w/ built-in caching & URL state management for Vue!
diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json
index b708dafc044..f6b5df788ad 100644
--- a/packages/vue-router/package.json
+++ b/packages/vue-router/package.json
@@ -72,6 +72,7 @@
"@tanstack/history": "workspace:*",
"@tanstack/router-core": "workspace:*",
"@tanstack/vue-store": "^0.8.0",
+ "@vue/runtime-dom": "^3.5.25",
"isbot": "^5.1.22",
"jsesc": "^3.0.2",
"tiny-invariant": "^1.3.3",
diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx
index 806fee28d52..998aecef2ad 100644
--- a/packages/vue-router/src/link.tsx
+++ b/packages/vue-router/src/link.tsx
@@ -19,39 +19,48 @@ import type {
RegisteredRouter,
RoutePaths,
} from '@tanstack/router-core'
+import type { AnchorHTMLAttributes, ReservedProps } from '@vue/runtime-dom'
import type {
ValidateLinkOptions,
ValidateLinkOptionsArray,
} from './typePrimitives'
-// Type definitions to replace missing Vue JSX types
type EventHandler = (e: TEvent) => void
-interface HTMLAttributes {
- class?: string
- style?: Record
- onClick?: EventHandler
- onFocus?: EventHandler
- // Vue 3's h() function expects lowercase event names after 'on' prefix
- onMouseenter?: EventHandler
- onMouseleave?: EventHandler
- onMouseover?: EventHandler
- onMouseout?: EventHandler
- onTouchstart?: EventHandler
- // Also accept the camelCase versions for external API compatibility
- onMouseEnter?: EventHandler
- onMouseLeave?: EventHandler
- onMouseOver?: EventHandler
- onMouseOut?: EventHandler
- onTouchStart?: EventHandler
- [key: string]: any
+
+type DataAttributes = {
+ [K in `data-${string}`]?: unknown
}
+type LinkHTMLAttributes = AnchorHTMLAttributes &
+ ReservedProps &
+ DataAttributes & {
+ // Vue's runtime-dom types use lowercase event names.
+ // Also accept camelCase versions for external API compatibility.
+ onMouseEnter?: EventHandler
+ onMouseLeave?: EventHandler
+ onMouseOver?: EventHandler
+ onMouseOut?: EventHandler
+ onTouchStart?: EventHandler
+
+ // `disabled` is not a valid attribute, but is useful when using `asChild`.
+ disabled?: boolean
+ }
+
interface StyledProps {
- class?: string
- style?: Record
- [key: string]: any
+ class?: LinkHTMLAttributes['class']
+ style?: LinkHTMLAttributes['style']
+ [key: string]: unknown
}
+type PropsOfComponent =
+ // Functional components
+ TComp extends (props: infer P, ...args: Array) => any
+ ? P
+ : // Vue components (defineComponent, class components, etc)
+ TComp extends Vue.Component
+ ? P
+ : Record
+
export function useLinkProps<
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends RoutePaths | string = string,
@@ -60,7 +69,7 @@ export function useLinkProps<
TMaskTo extends string = '',
>(
options: UseLinkPropsOptions,
-): HTMLAttributes {
+): LinkHTMLAttributes {
const router = useRouter()
const isTransitioning = Vue.ref(false)
let hasRenderFetched = false
@@ -195,6 +204,7 @@ export function useLinkProps<
// Create safe props that can be spread
const getPropsSafeToSpread = () => {
const result: Record = {}
+ const optionRecord = options as unknown as Record
for (const key in options) {
if (
![
@@ -233,7 +243,7 @@ export function useLinkProps<
'additionalProps',
].includes(key)
) {
- result[key] = options[key]
+ result[key] = optionRecord[key]
}
}
return result
@@ -241,7 +251,7 @@ export function useLinkProps<
if (type.value === 'external') {
// External links just have simple props
- const externalProps: HTMLAttributes = {
+ const externalProps: Record = {
...getPropsSafeToSpread(),
ref,
href: options.to,
@@ -265,11 +275,11 @@ export function useLinkProps<
}
})
- return externalProps
+ return externalProps as LinkHTMLAttributes
}
// The click handler
- const handleClick = (e: MouseEvent): void => {
+ const handleClick = (e: PointerEvent): void => {
// Check actual element's target attribute as fallback
const elementTarget = (
e.currentTarget as HTMLAnchorElement | SVGAElement
@@ -307,7 +317,7 @@ export function useLinkProps<
startTransition: options.startTransition,
viewTransition: options.viewTransition,
ignoreBlocker: options.ignoreBlocker,
- } as any)
+ })
}
}
@@ -448,7 +458,7 @@ export function useLinkProps<
// Create static event handlers that don't change between renders
const staticEventHandlers = {
- onClick: composeEventHandlers([
+ onClick: composeEventHandlers([
options.onClick,
handleClick,
]) as any,
@@ -480,8 +490,8 @@ export function useLinkProps<
// Compute all props synchronously to avoid hydration mismatches
// Using Vue.computed ensures props are calculated at render time, not after
- const computedProps = Vue.computed(() => {
- const result: HTMLAttributes = {
+ const computedProps = Vue.computed(() => {
+ const result: Record = {
...getPropsSafeToSpread(),
href: href.value,
ref,
@@ -523,20 +533,20 @@ export function useLinkProps<
for (const key of Object.keys(activeP)) {
if (key !== 'class' && key !== 'style') {
- result[key] = activeP[key]
+ result[key] = (activeP as any)[key]
}
}
for (const key of Object.keys(inactiveP)) {
if (key !== 'class' && key !== 'style') {
- result[key] = inactiveP[key]
+ result[key] = (inactiveP as any)[key]
}
}
- return result
+ return result as LinkHTMLAttributes
})
// Return the computed ref itself - callers should access .value
- return computedProps as unknown as HTMLAttributes
+ return computedProps as unknown as LinkHTMLAttributes
}
// Type definitions
@@ -547,7 +557,7 @@ export type UseLinkPropsOptions<
TMaskFrom extends RoutePaths | string = TFrom,
TMaskTo extends string = '.',
> = ActiveLinkOptions<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
- HTMLAttributes
+ LinkHTMLAttributes
export type ActiveLinkOptions<
TComp = 'a',
@@ -560,7 +570,9 @@ export type ActiveLinkOptions<
ActiveLinkOptionProps
type ActiveLinkProps = Partial<
- HTMLAttributes & {
+ (TComp extends keyof HTMLElementTagNameMap
+ ? LinkHTMLAttributes
+ : PropsOfComponent) & {
[key: `data-${string}`]: unknown
}
>
@@ -591,15 +603,16 @@ export type LinkProps<
export interface LinkPropsChildren {
// If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
children?:
- | Vue.VNode
- | ((state: { isActive: boolean; isTransitioning: boolean }) => Vue.VNode)
+ | Vue.VNodeChild
+ | ((state: {
+ isActive: boolean
+ isTransitioning: boolean
+ }) => Vue.VNodeChild)
}
type LinkComponentVueProps = TComp extends keyof HTMLElementTagNameMap
- ? Omit
- : TComp extends Vue.Component
- ? Record
- : Record
+ ? Omit
+ : Omit, keyof CreateLinkProps>
export type LinkComponentProps<
TComp = 'a',
@@ -620,9 +633,12 @@ export type CreateLinkProps = LinkProps<
string
>
-export type LinkComponent = <
+export type LinkComponent<
+ in out TComp,
+ in out TDefaultFrom extends string = string,
+> = <
TRouter extends AnyRouter = RegisteredRouter,
- const TFrom extends string = string,
+ const TFrom extends string = TDefaultFrom,
const TTo extends string | undefined = undefined,
const TMaskFrom extends string = TFrom,
const TMaskTo extends string = '',
@@ -657,7 +673,7 @@ export function createLink(
name: 'CreatedLink',
inheritAttrs: false,
setup(_, { attrs, slots }) {
- return () => Vue.h(Link, { ...attrs, _asChild: Comp }, slots)
+ return () => Vue.h(LinkImpl as any, { ...attrs, _asChild: Comp }, slots)
},
}) as any
}
@@ -696,7 +712,7 @@ const LinkImpl = Vue.defineComponent({
const allProps = { ...props, ...attrs }
const linkPropsComputed = useLinkProps(
allProps as any,
- ) as unknown as Vue.ComputedRef
+ ) as unknown as Vue.ComputedRef
return () => {
const Component = props._asChild || 'a'
@@ -720,7 +736,7 @@ const LinkImpl = Vue.defineComponent({
if (Component === 'svg') {
// Create props without class for svg link
const svgLinkProps = { ...linkProps }
- delete (svgLinkProps as any).class
+ delete svgLinkProps.class
return Vue.h('svg', {}, [Vue.h('a', svgLinkProps, slotContent)])
}
@@ -743,17 +759,9 @@ const LinkImpl = Vue.defineComponent({
/**
* Link component with proper TypeScript generics support
*/
-export const Link = LinkImpl as unknown as {
- <
- TRouter extends AnyRouter = RegisteredRouter,
- TFrom extends RoutePaths | string = string,
- TTo extends string | undefined = '.',
- TMaskFrom extends RoutePaths | string = TFrom,
- TMaskTo extends string = '.',
- >(
- props: LinkComponentProps<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
- ): Vue.VNode
-}
+export const Link = LinkImpl as unknown as Vue.Component &
+ Vue.Component &
+ LinkComponent<'a'>
function isCtrlEvent(e: MouseEvent) {
return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
diff --git a/packages/vue-router/src/ssr/renderRouterToStream.tsx b/packages/vue-router/src/ssr/renderRouterToStream.tsx
index ed2a5e6de32..d8649ff1bdf 100644
--- a/packages/vue-router/src/ssr/renderRouterToStream.tsx
+++ b/packages/vue-router/src/ssr/renderRouterToStream.tsx
@@ -14,7 +14,7 @@ function prependDoctype(
let sentDoctype = false
return new NodeReadableStream({
- async start(controller) {
+ start(controller) {
const reader = readable.getReader()
async function pump(): Promise {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 224c8db4305..bfee5c21683 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1458,7 +1458,7 @@ importers:
version: 2.6.0
vite:
specifier: ^7.1.7
- version: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ 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)
devDependencies:
'@playwright/test':
specifier: ^1.56.1
@@ -1477,7 +1477,7 @@ importers:
version: 19.2.2(@types/react@19.2.2)
'@vitejs/plugin-react':
specifier: ^4.3.4
- version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ version: 4.7.0(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))
autoprefixer:
specifier: ^10.4.20
version: 10.4.20(postcss@8.5.6)
@@ -1492,7 +1492,7 @@ importers:
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@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ 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))
e2e/react-start/custom-basepath:
dependencies:
@@ -4965,6 +4965,82 @@ importers:
specifier: ^3.1.8
version: 3.1.8(typescript@5.9.2)
+ e2e/vue-start/server-functions:
+ 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.9.8
+ version: 0.9.8
+ 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))
+
e2e/vue-start/server-routes:
dependencies:
'@tanstack/vue-query':
@@ -5029,8 +5105,8 @@ importers:
specifier: ^8.5.1
version: 8.5.6
srvx:
- specifier: ^0.8.6
- version: 0.8.15
+ specifier: ^0.9.8
+ version: 0.9.8
tailwindcss:
specifier: ^4.1.17
version: 4.1.17
@@ -7523,7 +7599,7 @@ importers:
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)
vitest:
specifier: ^3.2.4
- version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.56.1)(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))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
web-vitals:
specifier: ^5.1.0
version: 5.1.0
@@ -9930,7 +10006,7 @@ importers:
devDependencies:
'@netlify/vite-plugin-tanstack-start':
specifier: ^1.1.4
- version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))
+ version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))
'@tailwindcss/postcss':
specifier: ^4.1.15
version: 4.1.15
@@ -10168,7 +10244,7 @@ importers:
version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(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))
vitest:
specifier: ^3.2.4
- version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.56.1)(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))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
web-vitals:
specifier: ^5.1.0
version: 5.1.0
@@ -10659,16 +10735,16 @@ importers:
devDependencies:
'@vitejs/plugin-vue':
specifier: ^5.2.3
- version: 5.2.4(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))
+ version: 5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(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: ^4.1.2
- version: 4.2.0(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))
+ version: 4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2))
typescript:
specifier: ^5.7.2
version: 5.9.2
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)
+ version: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
vue-tsc:
specifier: ^3.1.5
version: 3.1.5(typescript@5.9.2)
@@ -11757,6 +11833,9 @@ importers:
'@tanstack/vue-store':
specifier: ^0.8.0
version: 0.8.0(vue@3.5.25(typescript@5.9.2))
+ '@vue/runtime-dom':
+ specifier: ^3.5.25
+ version: 3.5.25
isbot:
specifier: ^5.1.22
version: 5.1.28
@@ -26191,13 +26270,13 @@ snapshots:
uuid: 11.1.0
write-file-atomic: 5.0.1
- '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)':
+ '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/config': 23.2.0
'@netlify/dev-utils': 4.3.0
'@netlify/edge-functions-dev': 1.0.0
- '@netlify/functions-dev': 1.0.0(rollup@4.52.5)
+ '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5)
'@netlify/headers': 2.1.0
'@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)
'@netlify/redirects': 3.1.0
@@ -26265,12 +26344,12 @@ snapshots:
dependencies:
'@netlify/types': 2.1.0
- '@netlify/functions-dev@1.0.0(rollup@4.52.5)':
+ '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/dev-utils': 4.3.0
'@netlify/functions': 5.0.0
- '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5)
+ '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1
@@ -26360,9 +26439,9 @@ snapshots:
'@netlify/types@2.1.0': {}
- '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))':
+ '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))':
dependencies:
- '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))
+ '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))
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)
optionalDependencies:
'@tanstack/solid-start': link:packages/solid-start
@@ -26390,9 +26469,9 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(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))':
+ '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(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))':
dependencies:
- '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)
+ '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)
'@netlify/dev-utils': 4.3.0
dedent: 1.7.0(babel-plugin-macros@3.1.0)
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)
@@ -26420,13 +26499,13 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)':
+ '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.4
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 2.7.1
- '@vercel/nft': 0.29.4(rollup@4.52.5)
+ '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5)
archiver: 7.0.1
common-path-prefix: 3.0.0
copy-file: 11.1.0
@@ -29502,7 +29581,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vercel/nft@0.29.4(rollup@4.52.5)':
+ '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13)
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)
@@ -29563,18 +29642,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitejs/plugin-react@4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
- dependencies:
- '@babel/core': 7.28.5
- '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
- '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
- '@rolldown/pluginutils': 1.0.0-beta.27
- '@types/babel__core': 7.20.5
- react-refresh: 0.17.0
- vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
- transitivePeerDependencies:
- - supports-color
-
'@vitejs/plugin-react@4.7.0(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))':
dependencies:
'@babel/core': 7.28.5
@@ -29611,6 +29678,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vitejs/plugin-vue-jsx@4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2))':
+ dependencies:
+ '@babel/core': 7.28.5
+ '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
+ '@rolldown/pluginutils': 1.0.0-beta.40
+ '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
+ vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vue: 3.5.25(typescript@5.9.2)
+ transitivePeerDependencies:
+ - supports-color
+
'@vitejs/plugin-vue-jsx@4.2.0(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.8.3))':
dependencies:
'@babel/core': 7.28.5
@@ -29645,6 +29723,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vitejs/plugin-vue@5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2))':
+ dependencies:
+ vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(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@5.2.4(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.8.3))':
dependencies:
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)
@@ -29908,7 +29991,7 @@ snapshots:
'@volar/language-core': 2.4.11
'@vue/compiler-dom': 3.5.14
'@vue/compiler-vue2': 2.7.16
- '@vue/shared': 3.5.14
+ '@vue/shared': 3.5.25
computeds: 0.0.1
minimatch: 9.0.5
muggle-string: 0.4.1
@@ -29921,7 +30004,7 @@ snapshots:
'@volar/language-core': 2.4.11
'@vue/compiler-dom': 3.5.14
'@vue/compiler-vue2': 2.7.16
- '@vue/shared': 3.5.14
+ '@vue/shared': 3.5.25
computeds: 0.0.1
minimatch: 9.0.5
muggle-string: 0.4.1
@@ -36477,17 +36560,6 @@ snapshots:
- supports-color
- typescript
- vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)):
- dependencies:
- debug: 4.4.3
- globrex: 0.1.2
- tsconfck: 3.1.4(typescript@5.9.2)
- optionalDependencies:
- vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
- transitivePeerDependencies:
- - supports-color
- - typescript
-
vite-tsconfig-paths@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)):
dependencies:
debug: 4.4.3
@@ -36537,7 +36609,7 @@ snapshots:
optionalDependencies:
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)
- vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1):
+ vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.56.1)(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))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
@@ -36566,7 +36638,7 @@ snapshots:
'@types/node': 22.10.2
'@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.56.1)(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))(vitest@3.2.4)
'@vitest/ui': 3.0.6(vitest@3.2.4)
- jsdom: 25.0.1
+ jsdom: 27.0.0(postcss@8.5.6)
transitivePeerDependencies:
- jiti
- less
@@ -36581,7 +36653,7 @@ snapshots:
- tsx
- yaml
- vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1):
+ vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
@@ -36610,7 +36682,7 @@ snapshots:
'@types/node': 22.10.2
'@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.56.1)(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))(vitest@3.2.4)
'@vitest/ui': 3.0.6(vitest@3.2.4)
- jsdom: 27.0.0(postcss@8.5.6)
+ jsdom: 25.0.1
transitivePeerDependencies:
- jiti
- less