Skip to content

Commit 4fcfdaa

Browse files
feat: add server fn meta (#6213)
* feat: add server fn meta * fix: resolve merge conflicts and fix eslint errors in start-*-core * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent e6862d1 commit 4fcfdaa

34 files changed

+547
-105
lines changed

e2e/react-start/server-functions/src/routeTree.gen.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Route as MiddlewareServerImportMiddlewareRouteImport } from './routes/m
4040
import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn'
4141
import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware'
4242
import { Route as MiddlewareMiddlewareFactoryRouteImport } from './routes/middleware/middleware-factory'
43+
import { Route as MiddlewareFunctionMetadataRouteImport } from './routes/middleware/function-metadata'
4344
import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router'
4445
import { Route as CookiesSetRouteImport } from './routes/cookies/set'
4546
import { Route as AbortSignalMethodRouteImport } from './routes/abort-signal/$method'
@@ -206,6 +207,12 @@ const MiddlewareMiddlewareFactoryRoute =
206207
path: '/middleware/middleware-factory',
207208
getParentRoute: () => rootRouteImport,
208209
} as any)
210+
const MiddlewareFunctionMetadataRoute =
211+
MiddlewareFunctionMetadataRouteImport.update({
212+
id: '/middleware/function-metadata',
213+
path: '/middleware/function-metadata',
214+
getParentRoute: () => rootRouteImport,
215+
} as any)
209216
const MiddlewareClientMiddlewareRouterRoute =
210217
MiddlewareClientMiddlewareRouterRouteImport.update({
211218
id: '/middleware/client-middleware-router',
@@ -261,6 +268,7 @@ export interface FileRoutesByFullPath {
261268
'/abort-signal/$method': typeof AbortSignalMethodRoute
262269
'/cookies/set': typeof CookiesSetRoute
263270
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
271+
'/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute
264272
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
265273
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
266274
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
@@ -300,6 +308,7 @@ export interface FileRoutesByTo {
300308
'/abort-signal/$method': typeof AbortSignalMethodRoute
301309
'/cookies/set': typeof CookiesSetRoute
302310
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
311+
'/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute
303312
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
304313
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
305314
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
@@ -340,6 +349,7 @@ export interface FileRoutesById {
340349
'/abort-signal/$method': typeof AbortSignalMethodRoute
341350
'/cookies/set': typeof CookiesSetRoute
342351
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
352+
'/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute
343353
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
344354
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
345355
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
@@ -381,6 +391,7 @@ export interface FileRouteTypes {
381391
| '/abort-signal/$method'
382392
| '/cookies/set'
383393
| '/middleware/client-middleware-router'
394+
| '/middleware/function-metadata'
384395
| '/middleware/middleware-factory'
385396
| '/middleware/request-middleware'
386397
| '/middleware/send-serverFn'
@@ -420,6 +431,7 @@ export interface FileRouteTypes {
420431
| '/abort-signal/$method'
421432
| '/cookies/set'
422433
| '/middleware/client-middleware-router'
434+
| '/middleware/function-metadata'
423435
| '/middleware/middleware-factory'
424436
| '/middleware/request-middleware'
425437
| '/middleware/send-serverFn'
@@ -459,6 +471,7 @@ export interface FileRouteTypes {
459471
| '/abort-signal/$method'
460472
| '/cookies/set'
461473
| '/middleware/client-middleware-router'
474+
| '/middleware/function-metadata'
462475
| '/middleware/middleware-factory'
463476
| '/middleware/request-middleware'
464477
| '/middleware/send-serverFn'
@@ -499,6 +512,7 @@ export interface RootRouteChildren {
499512
AbortSignalMethodRoute: typeof AbortSignalMethodRoute
500513
CookiesSetRoute: typeof CookiesSetRoute
501514
MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
515+
MiddlewareFunctionMetadataRoute: typeof MiddlewareFunctionMetadataRoute
502516
MiddlewareMiddlewareFactoryRoute: typeof MiddlewareMiddlewareFactoryRoute
503517
MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute
504518
MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute
@@ -738,6 +752,13 @@ declare module '@tanstack/react-router' {
738752
preLoaderRoute: typeof MiddlewareMiddlewareFactoryRouteImport
739753
parentRoute: typeof rootRouteImport
740754
}
755+
'/middleware/function-metadata': {
756+
id: '/middleware/function-metadata'
757+
path: '/middleware/function-metadata'
758+
fullPath: '/middleware/function-metadata'
759+
preLoaderRoute: typeof MiddlewareFunctionMetadataRouteImport
760+
parentRoute: typeof rootRouteImport
761+
}
741762
'/middleware/client-middleware-router': {
742763
id: '/middleware/client-middleware-router'
743764
path: '/middleware/client-middleware-router'
@@ -803,6 +824,7 @@ const rootRouteChildren: RootRouteChildren = {
803824
AbortSignalMethodRoute: AbortSignalMethodRoute,
804825
CookiesSetRoute: CookiesSetRoute,
805826
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
827+
MiddlewareFunctionMetadataRoute: MiddlewareFunctionMetadataRoute,
806828
MiddlewareMiddlewareFactoryRoute: MiddlewareMiddlewareFactoryRoute,
807829
MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute,
808830
MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
import { createMiddleware, createServerFn } from '@tanstack/react-start'
3+
import React from 'react'
4+
5+
const metadataMiddleware = createMiddleware({ type: 'function' })
6+
.client(async ({ next, serverFnMeta }) => {
7+
return next({
8+
sendContext: {
9+
clientCapturedMeta: serverFnMeta,
10+
},
11+
})
12+
})
13+
.server(async ({ next, serverFnMeta, context }) => {
14+
return next({
15+
context: {
16+
serverCapturedMeta: serverFnMeta,
17+
clientCapturedMeta: context.clientCapturedMeta,
18+
},
19+
})
20+
})
21+
22+
// Server function that returns both client and server captured metadata
23+
const getMetadataFn = createServerFn()
24+
.middleware([metadataMiddleware])
25+
.handler(async ({ context }) => {
26+
return {
27+
// Full metadata captured by server middleware
28+
serverMeta: context.serverCapturedMeta,
29+
// Metadata captured by client middleware and sent via sendContext
30+
// Client middleware only has { id }, not { name, filename }
31+
clientCapturedMeta: context.clientCapturedMeta,
32+
}
33+
})
34+
35+
export const Route = createFileRoute('/middleware/function-metadata')({
36+
loader: () => getMetadataFn(),
37+
component: RouteComponent,
38+
})
39+
40+
function RouteComponent() {
41+
const loaderData = Route.useLoaderData()
42+
43+
const [clientData, setClientData] = React.useState<typeof loaderData | null>(
44+
null,
45+
)
46+
47+
return (
48+
<div>
49+
<h2>Function Metadata in Middleware</h2>
50+
<p>
51+
This test verifies that both client and server middleware receive
52+
serverFnMeta in their options. Client middleware gets only the id, while
53+
server middleware gets the full metadata (id, name, filename).
54+
</p>
55+
<br />
56+
<div>
57+
<div data-testid="loader-data">
58+
<h3>Loader Data (SSR)</h3>
59+
<h4>Server Captured Metadata:</h4>
60+
<div>
61+
Function ID:{' '}
62+
<span data-testid="loader-function-id">
63+
{loaderData.serverMeta?.id}
64+
</span>
65+
</div>
66+
<div>
67+
Function Name:{' '}
68+
<span data-testid="loader-function-name">
69+
{loaderData.serverMeta?.name}
70+
</span>
71+
</div>
72+
<div>
73+
Filename:{' '}
74+
<span data-testid="loader-filename">
75+
{loaderData.serverMeta?.filename}
76+
</span>
77+
</div>
78+
<h4>Client Captured Metadata (via sendContext):</h4>
79+
<p>Client middleware only receives id, not name or filename:</p>
80+
<div>
81+
Client Captured ID:{' '}
82+
<span data-testid="loader-client-captured-id">
83+
{loaderData.clientCapturedMeta?.id}
84+
</span>
85+
</div>
86+
<div>
87+
Client Captured Name:{' '}
88+
<span data-testid="loader-client-captured-name">
89+
{/* Cast to any to test that name is not present at runtime */}
90+
{(loaderData.clientCapturedMeta as any)?.name ?? 'undefined'}
91+
</span>
92+
</div>
93+
<div>
94+
Client Captured Filename:{' '}
95+
<span data-testid="loader-client-captured-filename">
96+
{/* Cast to any to test that filename is not present at runtime */}
97+
{(loaderData.clientCapturedMeta as any)?.filename ?? 'undefined'}
98+
</span>
99+
</div>
100+
</div>
101+
<br />
102+
<div>
103+
<button
104+
data-testid="call-server-fn-btn"
105+
onClick={async () => {
106+
const data = await getMetadataFn()
107+
setClientData(data)
108+
}}
109+
>
110+
Call server function from client
111+
</button>
112+
</div>
113+
<br />
114+
{clientData && (
115+
<div data-testid="client-data">
116+
<h3>Client Data</h3>
117+
<h4>Server Captured Metadata:</h4>
118+
<div>
119+
Function ID:{' '}
120+
<span data-testid="client-function-id">
121+
{clientData.serverMeta?.id}
122+
</span>
123+
</div>
124+
<div>
125+
Function Name:{' '}
126+
<span data-testid="client-function-name">
127+
{clientData.serverMeta?.name}
128+
</span>
129+
</div>
130+
<div>
131+
Filename:{' '}
132+
<span data-testid="client-filename">
133+
{clientData.serverMeta?.filename}
134+
</span>
135+
</div>
136+
<h4>Client Captured Metadata (via sendContext):</h4>
137+
<p>Client middleware only receives id, not name or filename:</p>
138+
<div>
139+
Client Captured ID:{' '}
140+
<span data-testid="client-client-captured-id">
141+
{clientData.clientCapturedMeta?.id}
142+
</span>
143+
</div>
144+
<div>
145+
Client Captured Name:{' '}
146+
<span data-testid="client-client-captured-name">
147+
{/* Cast to any to test that name is not present at runtime */}
148+
{(clientData.clientCapturedMeta as any)?.name ?? 'undefined'}
149+
</span>
150+
</div>
151+
<div>
152+
Client Captured Filename:{' '}
153+
<span data-testid="client-client-captured-filename">
154+
{/* Cast to any to test that filename is not present at runtime */}
155+
{(clientData.clientCapturedMeta as any)?.filename ??
156+
'undefined'}
157+
</span>
158+
</div>
159+
</div>
160+
)}
161+
</div>
162+
</div>
163+
)
164+
}

e2e/react-start/server-functions/src/routes/middleware/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ function RouteComponent() {
5858
Redirect via server function with middleware
5959
</Route.Link>
6060
</li>
61+
<li>
62+
<Route.Link
63+
to="./function-metadata"
64+
data-testid="function-metadata-link"
65+
>
66+
Function middleware receives functionId and filename
67+
</Route.Link>
68+
</li>
6169
</ul>
6270
</div>
6371
)

e2e/react-start/server-functions/tests/server-functions.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,3 +757,86 @@ test.describe('unhandled exception in middleware (issue #5266)', () => {
757757
await expect(page.getByTestId('route-success')).not.toBeVisible()
758758
})
759759
})
760+
761+
test('function middleware receives serverFnMeta in options', async ({
762+
page,
763+
}) => {
764+
// This test verifies that:
765+
// 1. Client middleware receives serverFnMeta with just { id } - NOT name or filename
766+
// 2. Server middleware receives serverFnMeta with full { id, name, filename }
767+
// 3. Client middleware can send the function metadata to the server via sendContext
768+
await page.goto('/middleware/function-metadata')
769+
770+
await page.waitForLoadState('networkidle')
771+
772+
// Verify SSR data - server captured metadata should have full properties
773+
const loaderFunctionId = await page
774+
.getByTestId('loader-function-id')
775+
.textContent()
776+
const loaderFunctionName = await page
777+
.getByTestId('loader-function-name')
778+
.textContent()
779+
const loaderFilename = await page.getByTestId('loader-filename').textContent()
780+
const loaderClientCapturedId = await page
781+
.getByTestId('loader-client-captured-id')
782+
.textContent()
783+
const loaderClientCapturedName = await page
784+
.getByTestId('loader-client-captured-name')
785+
.textContent()
786+
const loaderClientCapturedFilename = await page
787+
.getByTestId('loader-client-captured-filename')
788+
.textContent()
789+
790+
// id should be a non-empty string
791+
expect(loaderFunctionId).toBeTruthy()
792+
expect(loaderFunctionId!.length).toBeGreaterThan(0)
793+
794+
// name should be the variable name of the server function
795+
expect(loaderFunctionName).toBeTruthy()
796+
expect(loaderFunctionName).toBe('getMetadataFn')
797+
798+
// filename should be the exact route file path
799+
expect(loaderFilename).toBe('src/routes/middleware/function-metadata.tsx')
800+
801+
// Client captured ID should match the server function id
802+
// (sent via client middleware's sendContext)
803+
expect(loaderClientCapturedId).toBe(loaderFunctionId)
804+
805+
// Client middleware should NOT have access to name or filename
806+
// These should be "undefined" (the fallback value we display in the UI)
807+
expect(loaderClientCapturedName).toBe('undefined')
808+
expect(loaderClientCapturedFilename).toBe('undefined')
809+
810+
// Now test client-side call
811+
await page.getByTestId('call-server-fn-btn').click()
812+
await page.waitForSelector('[data-testid="client-data"]')
813+
814+
const clientFunctionId = await page
815+
.getByTestId('client-function-id')
816+
.textContent()
817+
const clientFunctionName = await page
818+
.getByTestId('client-function-name')
819+
.textContent()
820+
const clientFilename = await page.getByTestId('client-filename').textContent()
821+
const clientClientCapturedId = await page
822+
.getByTestId('client-client-captured-id')
823+
.textContent()
824+
const clientClientCapturedName = await page
825+
.getByTestId('client-client-captured-name')
826+
.textContent()
827+
const clientClientCapturedFilename = await page
828+
.getByTestId('client-client-captured-filename')
829+
.textContent()
830+
831+
// Client call should get the same server metadata
832+
expect(clientFunctionId).toBe(loaderFunctionId)
833+
expect(clientFunctionName).toBe(loaderFunctionName)
834+
expect(clientFilename).toBe(loaderFilename)
835+
836+
// Client captured ID from client middleware should also match
837+
expect(clientClientCapturedId).toBe(loaderFunctionId)
838+
839+
// Client middleware should NOT have access to name or filename
840+
expect(clientClientCapturedName).toBe('undefined')
841+
expect(clientClientCapturedFilename).toBe('undefined')
842+
})
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { TSS_SERVER_FUNCTION } from '../constants'
22
import { serverFnFetcher } from './serverFnFetcher'
3+
import type { ClientFnMeta } from '../constants'
34

45
export function createClientRpc(functionId: string) {
56
const url = process.env.TSS_SERVER_FN_BASE + functionId
7+
const serverFnMeta: ClientFnMeta = { id: functionId }
68

79
const clientFn = (...args: Array<any>) => {
810
return serverFnFetcher(url, args, fetch)
911
}
1012

1113
return Object.assign(clientFn, {
1214
url,
13-
functionId,
15+
serverFnMeta,
1416
[TSS_SERVER_FUNCTION]: true,
1517
})
1618
}

0 commit comments

Comments
 (0)