diff --git a/e2e/react-start/server-functions/src/router.tsx b/e2e/react-start/server-functions/src/router.tsx index 5a9a35733d5..b773cd79c9d 100644 --- a/e2e/react-start/server-functions/src/router.tsx +++ b/e2e/react-start/server-functions/src/router.tsx @@ -1,9 +1,9 @@ import { createRouter } from '@tanstack/react-router' +import { QueryClient } from '@tanstack/react-query' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' import { routeTree } from './routeTree.gen' import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' import { NotFound } from './components/NotFound' -import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' -import { QueryClient } from '@tanstack/react-query' export function getRouter() { const queryClient = new QueryClient() diff --git a/e2e/solid-start/query-integration/src/routes/loader-fetchQuery/$type.tsx b/e2e/solid-start/query-integration/src/routes/loader-fetchQuery/$type.tsx index 59834271dc7..2a4db2f5605 100644 --- a/e2e/solid-start/query-integration/src/routes/loader-fetchQuery/$type.tsx +++ b/e2e/solid-start/query-integration/src/routes/loader-fetchQuery/$type.tsx @@ -16,7 +16,6 @@ export const Route = createFileRoute('/loader-fetchQuery/$type')({ context: ({ params }) => ({ queryOptions: makeQueryOptions(`loader-fetchQuery-${params.type}`), }), - loader: ({ context, params }) => { const queryPromise = context.queryClient.fetchQuery(context.queryOptions) if (params.type === 'sync') { @@ -30,7 +29,6 @@ function RouteComponent() { const loaderData = Route.useLoaderData() const context = Route.useRouteContext() const query = useQuery(() => context().queryOptions) - return (
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()} + +

+ +
+ ) +} +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}
+          
+
+
+
{ + if (mode() === 'js') { + evt.preventDefault() + const data = new FormData(evt.currentTarget) + await greetUserFn({ data }) + } + }} + > + + +
+
+ ) +} 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)} + +

+ +
+ ) +} 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} +
+
+
+
+ +
+
+
+

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)} + +

+ +
+ ) +} 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