diff --git a/e2e/solid-start/server-functions/package.json b/e2e/solid-start/server-functions/package.json
index 3dc94b9338c..49dc7041069 100644
--- a/e2e/solid-start/server-functions/package.json
+++ b/e2e/solid-start/server-functions/package.json
@@ -11,8 +11,10 @@
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
},
"dependencies": {
+ "@tanstack/solid-query": "^5.90.6",
"@tanstack/solid-router": "workspace:^",
"@tanstack/solid-router-devtools": "workspace:^",
+ "@tanstack/solid-router-ssr-query": "workspace:^",
"@tanstack/solid-start": "workspace:^",
"js-cookie": "^3.0.5",
"redaxios": "^0.5.1",
diff --git a/e2e/solid-start/server-functions/src/routeTree.gen.ts b/e2e/solid-start/server-functions/src/routeTree.gen.ts
index a1b7f601e88..977f8bcec4a 100644
--- a/e2e/solid-start/server-functions/src/routeTree.gen.ts
+++ b/e2e/solid-start/server-functions/src/routeTree.gen.ts
@@ -22,8 +22,16 @@ import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserv
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 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 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',
@@ -90,16 +98,59 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
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 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
@@ -116,7 +167,15 @@ export interface FileRoutesByFullPath {
'/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
'/cookies': typeof CookiesIndexRoute
+ '/factory': typeof FactoryIndexRoute
+ '/formdata-redirect': typeof FormdataRedirectIndexRoute
+ '/middleware': typeof MiddlewareIndexRoute
+ '/primitives': typeof PrimitivesIndexRoute
+ '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
@@ -133,7 +192,15 @@ export interface FileRoutesByTo {
'/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
'/cookies': typeof CookiesIndexRoute
+ '/factory': typeof FactoryIndexRoute
+ '/formdata-redirect': typeof FormdataRedirectIndexRoute
+ '/middleware': typeof MiddlewareIndexRoute
+ '/primitives': typeof PrimitivesIndexRoute
+ '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -151,7 +218,15 @@ export interface FileRoutesById {
'/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
'/cookies/': typeof CookiesIndexRoute
+ '/factory/': typeof FactoryIndexRoute
+ '/formdata-redirect/': typeof FormdataRedirectIndexRoute
+ '/middleware/': typeof MiddlewareIndexRoute
+ '/primitives/': typeof PrimitivesIndexRoute
+ '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -170,7 +245,15 @@ export interface FileRouteTypes {
| '/status'
| '/submit-post-formdata'
| '/cookies/set'
+ | '/middleware/client-middleware-router'
+ | '/middleware/request-middleware'
+ | '/middleware/send-serverFn'
| '/cookies'
+ | '/factory'
+ | '/formdata-redirect'
+ | '/middleware'
+ | '/primitives'
+ | '/formdata-redirect/target/$name'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -187,7 +270,15 @@ export interface FileRouteTypes {
| '/status'
| '/submit-post-formdata'
| '/cookies/set'
+ | '/middleware/client-middleware-router'
+ | '/middleware/request-middleware'
+ | '/middleware/send-serverFn'
| '/cookies'
+ | '/factory'
+ | '/formdata-redirect'
+ | '/middleware'
+ | '/primitives'
+ | '/formdata-redirect/target/$name'
id:
| '__root__'
| '/'
@@ -204,7 +295,15 @@ export interface FileRouteTypes {
| '/status'
| '/submit-post-formdata'
| '/cookies/set'
+ | '/middleware/client-middleware-router'
+ | '/middleware/request-middleware'
+ | '/middleware/send-serverFn'
| '/cookies/'
+ | '/factory/'
+ | '/formdata-redirect/'
+ | '/middleware/'
+ | '/primitives/'
+ | '/formdata-redirect/target/$name'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -222,7 +321,15 @@ export interface RootRouteChildren {
StatusRoute: typeof StatusRoute
SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute
CookiesSetRoute: typeof CookiesSetRoute
+ MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
+ MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute
+ MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute
CookiesIndexRoute: typeof CookiesIndexRoute
+ FactoryIndexRoute: typeof FactoryIndexRoute
+ FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute
+ MiddlewareIndexRoute: typeof MiddlewareIndexRoute
+ PrimitivesIndexRoute: typeof PrimitivesIndexRoute
+ FormdataRedirectTargetNameRoute: typeof FormdataRedirectTargetNameRoute
}
declare module '@tanstack/solid-router' {
@@ -318,6 +425,34 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof IndexRouteImport
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'
@@ -325,6 +460,27 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof CookiesIndexRouteImport
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'
@@ -332,6 +488,13 @@ declare module '@tanstack/solid-router' {
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
+ }
}
}
@@ -350,7 +513,15 @@ const rootRouteChildren: RootRouteChildren = {
StatusRoute: StatusRoute,
SubmitPostFormdataRoute: SubmitPostFormdataRoute,
CookiesSetRoute: CookiesSetRoute,
+ MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
+ MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute,
+ MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
CookiesIndexRoute: CookiesIndexRoute,
+ FactoryIndexRoute: FactoryIndexRoute,
+ FormdataRedirectIndexRoute: FormdataRedirectIndexRoute,
+ MiddlewareIndexRoute: MiddlewareIndexRoute,
+ PrimitivesIndexRoute: PrimitivesIndexRoute,
+ FormdataRedirectTargetNameRoute: FormdataRedirectTargetNameRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/e2e/solid-start/server-functions/src/router.tsx b/e2e/solid-start/server-functions/src/router.tsx
index da050c7db65..e5566c46174 100644
--- a/e2e/solid-start/server-functions/src/router.tsx
+++ b/e2e/solid-start/server-functions/src/router.tsx
@@ -1,17 +1,27 @@
import { createRouter } from '@tanstack/solid-router'
+import { setupRouterSsrQueryIntegration } from '@tanstack/solid-router-ssr-query'
+import { QueryClient } from '@tanstack/solid-query'
import { routeTree } from './routeTree.gen'
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
import { NotFound } from './components/NotFound'
export function getRouter() {
+ const queryClient = new QueryClient()
const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () =>
,
scrollRestoration: true,
+ context: {
+ foo: {
+ bar: 'baz',
+ },
+ },
})
+ setupRouterSsrQueryIntegration({ router, queryClient })
+
return router
}
diff --git a/e2e/solid-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts b/e2e/solid-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts
new file mode 100644
index 00000000000..3a51cdb7082
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts
@@ -0,0 +1,22 @@
+import { createMiddleware } from '@tanstack/solid-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/solid-start/server-functions/src/routes/factory/-functions/createFakeFn.ts b/e2e/solid-start/server-functions/src/routes/factory/-functions/createFakeFn.ts
new file mode 100644
index 00000000000..1c727338850
--- /dev/null
+++ b/e2e/solid-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/solid-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts b/e2e/solid-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts
new file mode 100644
index 00000000000..771d415a14f
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts
@@ -0,0 +1,22 @@
+import { createMiddleware, createServerFn } from '@tanstack/solid-start'
+
+const fooMiddleware = createMiddleware({ type: 'function' }).server(
+ ({ next }) => {
+ console.log('Foo middleware triggered')
+ return next({
+ context: { foo: 'foo' } as const,
+ })
+ },
+)
+
+export const createFooServerFn = createServerFn().middleware([fooMiddleware])
+
+export const fooFnInsideFactoryFile = createFooServerFn().handler(
+ async ({ context, method }) => {
+ console.log('fooFnInsideFactoryFile handler triggered', method)
+ return {
+ name: 'fooFnInsideFactoryFile',
+ context,
+ }
+ },
+)
diff --git a/e2e/solid-start/server-functions/src/routes/factory/-functions/functions.ts b/e2e/solid-start/server-functions/src/routes/factory/-functions/functions.ts
new file mode 100644
index 00000000000..2af5736775f
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/factory/-functions/functions.ts
@@ -0,0 +1,93 @@
+import { createMiddleware, createServerFn } from '@tanstack/solid-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/solid-start/server-functions/src/routes/factory/index.tsx b/e2e/solid-start/server-functions/src/routes/factory/index.tsx
new file mode 100644
index 00000000000..e5770f495a9
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/factory/index.tsx
@@ -0,0 +1,184 @@
+import { createFileRoute, deepEqual } from '@tanstack/solid-router'
+
+import { createSignal, For } from 'solid-js'
+import { createServerFn } from '@tanstack/solid-start'
+import { fooFnInsideFactoryFile } from './-functions/createFooServerFn'
+import {
+ barFn,
+ barFnPOST,
+ composedFn,
+ fakeFn,
+ fooFn,
+ fooFnPOST,
+ localFn,
+ localFnPOST,
+} from './-functions/functions'
+
+export const Route = createFileRoute('/factory/')({
+ ssr: false,
+ component: RouteComponent,
+})
+
+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' },
+ },
+ },
+ fooFn: {
+ fn: fooFn,
+ type: 'serverFn',
+
+ expected: {
+ name: 'fooFn',
+ context: { foo: 'foo' },
+ },
+ },
+ fooFnPOST: {
+ fn: fooFnPOST,
+ type: 'serverFn',
+
+ expected: {
+ name: 'fooFnPOST',
+ context: { foo: 'foo' },
+ },
+ },
+ barFn: {
+ fn: barFn,
+ type: 'serverFn',
+
+ expected: {
+ name: 'barFn',
+ context: { foo: 'foo', bar: 'bar' },
+ },
+ },
+ barFnPOST: {
+ fn: barFnPOST,
+ type: 'serverFn',
+
+ expected: {
+ name: 'barFnPOST',
+ context: { foo: 'foo', bar: 'bar' },
+ },
+ },
+ localFn: {
+ fn: localFn,
+ type: 'serverFn',
+
+ expected: {
+ name: 'localFn',
+ context: { foo: 'foo', bar: 'bar', local: 'local', another: 'another' },
+ },
+ },
+ localFnPOST: {
+ fn: localFnPOST,
+ type: 'serverFn',
+
+ expected: {
+ name: 'localFnPOST',
+ context: { foo: 'foo', bar: 'bar', local: 'local', another: 'another' },
+ },
+ },
+ composedFn: {
+ fn: composedFn,
+ type: 'serverFn',
+ expected: {
+ name: 'composedFn',
+ context: { foo: 'foo', 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'
+}
+function Test(props: TestCase) {
+ const [result, setResult] = createSignal(null)
+ function comparison() {
+ if (result()) {
+ const isEqual = deepEqual(result(), props.expected)
+ return isEqual ? 'equal' : 'not equal'
+ }
+ return 'Loading...'
+ }
+
+ return (
+
+
+
+ It should return{' '}
+
+
+ {props.type === 'serverFn' ? JSON.stringify(props.expected) : 'localFn'}
+
+
+
+
+ fn returns:
+
+
+ {result()
+ ? props.type === 'serverFn'
+ ? JSON.stringify(result())
+ : 'localFn'
+ : 'Loading...'}
+ {' '}
+
+ {comparison()}
+
+
+
{
+ props.fn().then(setResult)
+ }}
+ >
+ Invoke Server Function
+
+
+ )
+}
+function RouteComponent() {
+ return (
+
+
+ Server functions middleware E2E tests
+
+
+ {([name, testCase]) => }
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/src/routes/formdata-redirect/index.tsx b/e2e/solid-start/server-functions/src/routes/formdata-redirect/index.tsx
new file mode 100644
index 00000000000..ae57daa043d
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/formdata-redirect/index.tsx
@@ -0,0 +1,74 @@
+import { createFileRoute, redirect } from '@tanstack/solid-router'
+import { createServerFn, useServerFn } from '@tanstack/solid-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/solid-start/server-functions/src/routes/formdata-redirect/target.$name.tsx b/e2e/solid-start/server-functions/src/routes/formdata-redirect/target.$name.tsx
new file mode 100644
index 00000000000..8fce6a321c8
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/formdata-redirect/target.$name.tsx
@@ -0,0 +1,15 @@
+import { createFileRoute } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/formdata-redirect/target/$name')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const params = Route.useParams()
+ return (
+
+ Hello{' '}
+ {params().name} !
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/src/routes/middleware/client-middleware-router.tsx b/e2e/solid-start/server-functions/src/routes/middleware/client-middleware-router.tsx
new file mode 100644
index 00000000000..7876f432f62
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/middleware/client-middleware-router.tsx
@@ -0,0 +1,79 @@
+import { createFileRoute, useRouter } from '@tanstack/solid-router'
+import {
+ createMiddleware,
+ createServerFn,
+ getRouterInstance,
+} from '@tanstack/solid-start'
+import { createSignal } from 'solid-js'
+
+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
+ })
+export const Route = createFileRoute('/middleware/client-middleware-router')({
+ component: RouteComponent,
+ loader: async () => ({ serverFnLoaderResult: await serverFn() }),
+})
+
+function RouteComponent() {
+ const [serverFnClientResult, setServerFnClientResult] = createSignal({})
+ 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())}
+
+
+
+ serverFn when invoked on the client returns:
+
+
+ {JSON.stringify(loaderData().serverFnLoaderResult)}
+
+
+
{
+ serverFn().then(setServerFnClientResult)
+ }}
+ >
+ Invoke Server Function
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/src/routes/middleware/index.tsx b/e2e/solid-start/server-functions/src/routes/middleware/index.tsx
new file mode 100644
index 00000000000..d6914b269c8
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/middleware/index.tsx
@@ -0,0 +1,39 @@
+import { createFileRoute } from '@tanstack/solid-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/solid-start/server-functions/src/routes/middleware/request-middleware.tsx b/e2e/solid-start/server-functions/src/routes/middleware/request-middleware.tsx
new file mode 100644
index 00000000000..9d25e6366c3
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/middleware/request-middleware.tsx
@@ -0,0 +1,83 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createMiddleware, createServerFn } from '@tanstack/solid-start'
+import { getRequest } from '@tanstack/solid-start/server'
+import { createSignal, Show } from 'solid-js'
+
+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 }
+ })
+
+export const Route = createFileRoute('/middleware/request-middleware')({
+ loader: () => serverFn(),
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const loaderData = Route.useLoaderData()
+
+ const [clientData, setClientData] = createSignal | null>(null)
+
+ return (
+
+
Request Middleware in combination with server function
+
+
+
+
Loader Data Request Param:
+
+ {loaderData().requestParam}
+
+ Request Func:
+
+ {loaderData().requestFunc}
+
+
+
+
+ {
+ const data = await serverFn()
+ setClientData(data)
+ }}
+ >
+ Call server function from client
+
+
+
+
+
Client Data
+
+ {(data) => (
+
+ Request Param:
+
+ {data().requestParam}
+
+ Request Func:
+
+ {data().requestFunc}
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/src/routes/middleware/send-serverFn.tsx b/e2e/solid-start/server-functions/src/routes/middleware/send-serverFn.tsx
new file mode 100644
index 00000000000..a0d375127c9
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/middleware/send-serverFn.tsx
@@ -0,0 +1,78 @@
+import { createFileRoute } from '@tanstack/solid-router'
+import { createMiddleware, createServerFn } from '@tanstack/solid-start'
+import { createSignal } from 'solid-js'
+
+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'
+})
+
+export const Route = createFileRoute('/middleware/send-serverFn')({
+ component: RouteComponent,
+ loader: async () => ({ serverFnLoaderResult: await fooFn() }),
+})
+
+function RouteComponent() {
+ const [serverFnClientResult, setServerFnClientResult] = createSignal({})
+ 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())}
+
+
+
+ serverFn when invoked on the client returns:
+
+
+ {JSON.stringify(loaderData().serverFnLoaderResult)}
+
+
+
{
+ fooFn().then(setServerFnClientResult)
+ }}
+ >
+ Invoke Server Function
+
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/src/routes/primitives/index.tsx b/e2e/solid-start/server-functions/src/routes/primitives/index.tsx
new file mode 100644
index 00000000000..35ce5b8c66f
--- /dev/null
+++ b/e2e/solid-start/server-functions/src/routes/primitives/index.tsx
@@ -0,0 +1,136 @@
+import { useQuery } from '@tanstack/solid-query'
+import { createFileRoute } from '@tanstack/solid-router'
+import { createServerFn } from '@tanstack/solid-start'
+import { For, Show } from 'solid-js'
+import { z } from 'zod'
+export const Route = createFileRoute('/primitives/')({
+ component: RouteComponent,
+ ssr: 'data-only',
+})
+
+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
+ }
+}
+
+interface TestProps extends PrimitiveComponentProps {
+ method: 'get' | 'post'
+}
+function Test(props: TestProps) {
+ const query = useQuery(() => ({
+ queryKey: [props.data.type, props.method],
+ queryFn: async () => {
+ const result = await props.serverFn[props.method]({
+ data: props.data.value,
+ })
+ if (result === undefined) {
+ return '$undefined'
+ }
+ return result
+ },
+ }))
+ const testId = `${props.method}-${props.data.type}`
+ return (
+
+
serverFn method={props.method}
+
expected
+
+ {stringify(props.data.value)}
+
+
result
+
+ {stringify(query.data)}
+
+
+ )
+}
+function PrimitiveComponent(props: PrimitiveComponentProps) {
+ return (
+
+
data type: {props.data.type}
+
+
+
+
+
+
+ )
+}
+
+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>
+
+function RouteComponent() {
+ return (
+
+ {(t) => }
+
+ )
+}
diff --git a/e2e/solid-start/server-functions/tests/server-functions.spec.ts b/e2e/solid-start/server-functions/tests/server-functions.spec.ts
index 2af3201cf48..77e2aba8629 100644
--- a/e2e/solid-start/server-functions/tests/server-functions.spec.ts
+++ b/e2e/solid-start/server-functions/tests/server-functions.spec.ts
@@ -4,6 +4,20 @@ 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,
}) => {
@@ -310,3 +324,153 @@ test('raw response', async ({ page }) => {
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)
+ }
+})
diff --git a/e2e/solid-start/server-functions/vite.config.ts b/e2e/solid-start/server-functions/vite.config.ts
index 1a2219f4435..cb489b543b1 100644
--- a/e2e/solid-start/server-functions/vite.config.ts
+++ b/e2e/solid-start/server-functions/vite.config.ts
@@ -3,6 +3,11 @@ import tsConfigPaths from 'vite-tsconfig-paths'
import { tanstackStart } from '@tanstack/solid-start/plugin/vite'
import viteSolid from 'vite-plugin-solid'
+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,
@@ -11,7 +16,15 @@ export default defineConfig({
tsConfigPaths({
projects: ['./tsconfig.json'],
}),
- tanstackStart(),
+ tanstackStart({
+ serverFns: {
+ generateFunctionId: (opts) => {
+ const id = `${opts.filename}/${opts.functionName}`
+ if (FUNCTIONS_WITH_CONSTANT_ID.includes(id)) return 'constant_id'
+ else return undefined
+ },
+ },
+ }),
viteSolid({ ssr: true }),
],
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 99f54f981da..e368064e9d2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2511,7 +2511,7 @@ importers:
version: 1.0.3(@rsbuild/core@1.2.4)
'@rsbuild/plugin-solid':
specifier: ^1.0.4
- version: 1.0.4(@babel/core@7.28.4)(@rsbuild/core@1.2.4)(solid-js@1.9.9)
+ version: 1.0.4(@babel/core@7.27.4)(@rsbuild/core@1.2.4)(solid-js@1.9.9)
'@tailwindcss/postcss':
specifier: ^4.1.15
version: 4.1.15
@@ -2557,7 +2557,7 @@ importers:
version: 1.0.3(@rsbuild/core@1.2.4)
'@rsbuild/plugin-solid':
specifier: ^1.0.4
- version: 1.0.4(@babel/core@7.27.4)(@rsbuild/core@1.2.4)(solid-js@1.9.9)
+ version: 1.0.4(@babel/core@7.28.4)(@rsbuild/core@1.2.4)(solid-js@1.9.9)
'@tailwindcss/postcss':
specifier: ^4.1.15
version: 4.1.15
@@ -3132,12 +3132,18 @@ importers:
e2e/solid-start/server-functions:
dependencies:
+ '@tanstack/solid-query':
+ specifier: ^5.90.6
+ version: 5.90.6(solid-js@1.9.9)
'@tanstack/solid-router':
specifier: workspace:^
version: link:../../../packages/solid-router
'@tanstack/solid-router-devtools':
specifier: workspace:^
version: link:../../../packages/solid-router-devtools
+ '@tanstack/solid-router-ssr-query':
+ specifier: workspace:*
+ version: link:../../../packages/solid-router-ssr-query
'@tanstack/solid-start':
specifier: workspace:*
version: link:../../../packages/solid-start