Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions e2e/react-start/server-functions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { Route as MiddlewareServerImportMiddlewareRouteImport } from './routes/m
import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn'
import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware'
import { Route as MiddlewareMiddlewareFactoryRouteImport } from './routes/middleware/middleware-factory'
import { Route as MiddlewareFunctionMetadataRouteImport } from './routes/middleware/function-metadata'
import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router'
import { Route as CookiesSetRouteImport } from './routes/cookies/set'
import { Route as AbortSignalMethodRouteImport } from './routes/abort-signal/$method'
Expand Down Expand Up @@ -191,6 +192,12 @@ const MiddlewareMiddlewareFactoryRoute =
path: '/middleware/middleware-factory',
getParentRoute: () => rootRouteImport,
} as any)
const MiddlewareFunctionMetadataRoute =
MiddlewareFunctionMetadataRouteImport.update({
id: '/middleware/function-metadata',
path: '/middleware/function-metadata',
getParentRoute: () => rootRouteImport,
} as any)
const MiddlewareClientMiddlewareRouterRoute =
MiddlewareClientMiddlewareRouterRouteImport.update({
id: '/middleware/client-middleware-router',
Expand Down Expand Up @@ -233,6 +240,7 @@ export interface FileRoutesByFullPath {
'/abort-signal/$method': typeof AbortSignalMethodRoute
'/cookies/set': typeof CookiesSetRoute
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
Expand Down Expand Up @@ -268,6 +276,7 @@ export interface FileRoutesByTo {
'/abort-signal/$method': typeof AbortSignalMethodRoute
'/cookies/set': typeof CookiesSetRoute
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
Expand Down Expand Up @@ -304,6 +313,7 @@ export interface FileRoutesById {
'/abort-signal/$method': typeof AbortSignalMethodRoute
'/cookies/set': typeof CookiesSetRoute
'/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute
'/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute
'/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute
'/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute
'/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute
Expand Down Expand Up @@ -341,6 +351,7 @@ export interface FileRouteTypes {
| '/abort-signal/$method'
| '/cookies/set'
| '/middleware/client-middleware-router'
| '/middleware/function-metadata'
| '/middleware/middleware-factory'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
Expand Down Expand Up @@ -376,6 +387,7 @@ export interface FileRouteTypes {
| '/abort-signal/$method'
| '/cookies/set'
| '/middleware/client-middleware-router'
| '/middleware/function-metadata'
| '/middleware/middleware-factory'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
Expand Down Expand Up @@ -411,6 +423,7 @@ export interface FileRouteTypes {
| '/abort-signal/$method'
| '/cookies/set'
| '/middleware/client-middleware-router'
| '/middleware/function-metadata'
| '/middleware/middleware-factory'
| '/middleware/request-middleware'
| '/middleware/send-serverFn'
Expand Down Expand Up @@ -447,6 +460,7 @@ export interface RootRouteChildren {
AbortSignalMethodRoute: typeof AbortSignalMethodRoute
CookiesSetRoute: typeof CookiesSetRoute
MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute
MiddlewareFunctionMetadataRoute: typeof MiddlewareFunctionMetadataRoute
MiddlewareMiddlewareFactoryRoute: typeof MiddlewareMiddlewareFactoryRoute
MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute
MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute
Expand Down Expand Up @@ -669,6 +683,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MiddlewareMiddlewareFactoryRouteImport
parentRoute: typeof rootRouteImport
}
'/middleware/function-metadata': {
id: '/middleware/function-metadata'
path: '/middleware/function-metadata'
fullPath: '/middleware/function-metadata'
preLoaderRoute: typeof MiddlewareFunctionMetadataRouteImport
parentRoute: typeof rootRouteImport
}
'/middleware/client-middleware-router': {
id: '/middleware/client-middleware-router'
path: '/middleware/client-middleware-router'
Expand Down Expand Up @@ -719,6 +740,7 @@ const rootRouteChildren: RootRouteChildren = {
AbortSignalMethodRoute: AbortSignalMethodRoute,
CookiesSetRoute: CookiesSetRoute,
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
MiddlewareFunctionMetadataRoute: MiddlewareFunctionMetadataRoute,
MiddlewareMiddlewareFactoryRoute: MiddlewareMiddlewareFactoryRoute,
MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute,
MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { createFileRoute } from '@tanstack/react-router'
import { createMiddleware, createServerFn } from '@tanstack/react-start'
import React from 'react'

const metadataMiddleware = createMiddleware({ type: 'function' })
.client(async ({ next, serverFnMeta }) => {
return next({
sendContext: {
clientCapturedMeta: serverFnMeta,
},
})
})
.server(async ({ next, serverFnMeta, context }) => {
return next({
context: {
serverCapturedMeta: serverFnMeta,
clientCapturedMeta: context.clientCapturedMeta,
},
})
})

// Server function that returns both client and server captured metadata
const getMetadataFn = createServerFn()
.middleware([metadataMiddleware])
.handler(async ({ context }) => {
return {
// Full metadata captured by server middleware
serverMeta: context.serverCapturedMeta,
// Metadata captured by client middleware and sent via sendContext
// Client middleware only has { id }, not { name, filename }
clientCapturedMeta: context.clientCapturedMeta,
}
})

export const Route = createFileRoute('/middleware/function-metadata')({
loader: () => getMetadataFn(),
component: RouteComponent,
})

function RouteComponent() {
const loaderData = Route.useLoaderData()

const [clientData, setClientData] = React.useState<typeof loaderData | null>(
null,
)

return (
<div>
<h2>Function Metadata in Middleware</h2>
<p>
This test verifies that both client and server middleware receive
serverFnMeta in their options. Client middleware gets only the id, while
server middleware gets the full metadata (id, name, filename).
</p>
<br />
<div>
<div data-testid="loader-data">
<h3>Loader Data (SSR)</h3>
<h4>Server Captured Metadata:</h4>
<div>
Function ID:{' '}
<span data-testid="loader-function-id">
{loaderData.serverMeta?.id}
</span>
</div>
<div>
Function Name:{' '}
<span data-testid="loader-function-name">
{loaderData.serverMeta?.name}
</span>
</div>
<div>
Filename:{' '}
<span data-testid="loader-filename">
{loaderData.serverMeta?.filename}
</span>
</div>
<h4>Client Captured Metadata (via sendContext):</h4>
<p>Client middleware only receives id, not name or filename:</p>
<div>
Client Captured ID:{' '}
<span data-testid="loader-client-captured-id">
{loaderData.clientCapturedMeta?.id}
</span>
</div>
<div>
Client Captured Name:{' '}
<span data-testid="loader-client-captured-name">
{/* Cast to any to test that name is not present at runtime */}
{(loaderData.clientCapturedMeta as any)?.name ?? 'undefined'}
</span>
</div>
<div>
Client Captured Filename:{' '}
<span data-testid="loader-client-captured-filename">
{/* Cast to any to test that filename is not present at runtime */}
{(loaderData.clientCapturedMeta as any)?.filename ?? 'undefined'}
</span>
</div>
</div>
<br />
<div>
<button
data-testid="call-server-fn-btn"
onClick={async () => {
const data = await getMetadataFn()
setClientData(data)
}}
>
Call server function from client
</button>
</div>
<br />
{clientData && (
<div data-testid="client-data">
<h3>Client Data</h3>
<h4>Server Captured Metadata:</h4>
<div>
Function ID:{' '}
<span data-testid="client-function-id">
{clientData.serverMeta?.id}
</span>
</div>
<div>
Function Name:{' '}
<span data-testid="client-function-name">
{clientData.serverMeta?.name}
</span>
</div>
<div>
Filename:{' '}
<span data-testid="client-filename">
{clientData.serverMeta?.filename}
</span>
</div>
<h4>Client Captured Metadata (via sendContext):</h4>
<p>Client middleware only receives id, not name or filename:</p>
<div>
Client Captured ID:{' '}
<span data-testid="client-client-captured-id">
{clientData.clientCapturedMeta?.id}
</span>
</div>
<div>
Client Captured Name:{' '}
<span data-testid="client-client-captured-name">
{/* Cast to any to test that name is not present at runtime */}
{(clientData.clientCapturedMeta as any)?.name ?? 'undefined'}
</span>
</div>
<div>
Client Captured Filename:{' '}
<span data-testid="client-client-captured-filename">
{/* Cast to any to test that filename is not present at runtime */}
{(clientData.clientCapturedMeta as any)?.filename ??
'undefined'}
</span>
</div>
</div>
)}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ function RouteComponent() {
build
</Route.Link>
</li>
<li>
<Route.Link
to="./function-metadata"
data-testid="function-metadata-link"
>
Function middleware receives functionId and filename
</Route.Link>
</li>
</ul>
</div>
)
Expand Down
83 changes: 83 additions & 0 deletions e2e/react-start/server-functions/tests/server-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,86 @@ test('middleware factories with server-only imports are stripped from client bui
'x-factory-two',
)
})

test('function middleware receives serverFnMeta in options', async ({
page,
}) => {
// This test verifies that:
// 1. Client middleware receives serverFnMeta with just { id } - NOT name or filename
// 2. Server middleware receives serverFnMeta with full { id, name, filename }
// 3. Client middleware can send the function metadata to the server via sendContext
await page.goto('/middleware/function-metadata')

await page.waitForLoadState('networkidle')

// Verify SSR data - server captured metadata should have full properties
const loaderFunctionId = await page
.getByTestId('loader-function-id')
.textContent()
const loaderFunctionName = await page
.getByTestId('loader-function-name')
.textContent()
const loaderFilename = await page.getByTestId('loader-filename').textContent()
const loaderClientCapturedId = await page
.getByTestId('loader-client-captured-id')
.textContent()
const loaderClientCapturedName = await page
.getByTestId('loader-client-captured-name')
.textContent()
const loaderClientCapturedFilename = await page
.getByTestId('loader-client-captured-filename')
.textContent()

// id should be a non-empty string
expect(loaderFunctionId).toBeTruthy()
expect(loaderFunctionId!.length).toBeGreaterThan(0)

// name should be the variable name of the server function
expect(loaderFunctionName).toBeTruthy()
expect(loaderFunctionName).toBe('getMetadataFn')

// filename should be the exact route file path
expect(loaderFilename).toBe('src/routes/middleware/function-metadata.tsx')

// Client captured ID should match the server function id
// (sent via client middleware's sendContext)
expect(loaderClientCapturedId).toBe(loaderFunctionId)

// Client middleware should NOT have access to name or filename
// These should be "undefined" (the fallback value we display in the UI)
expect(loaderClientCapturedName).toBe('undefined')
expect(loaderClientCapturedFilename).toBe('undefined')

// Now test client-side call
await page.getByTestId('call-server-fn-btn').click()
await page.waitForSelector('[data-testid="client-data"]')

const clientFunctionId = await page
.getByTestId('client-function-id')
.textContent()
const clientFunctionName = await page
.getByTestId('client-function-name')
.textContent()
const clientFilename = await page.getByTestId('client-filename').textContent()
const clientClientCapturedId = await page
.getByTestId('client-client-captured-id')
.textContent()
const clientClientCapturedName = await page
.getByTestId('client-client-captured-name')
.textContent()
const clientClientCapturedFilename = await page
.getByTestId('client-client-captured-filename')
.textContent()

// Client call should get the same server metadata
expect(clientFunctionId).toBe(loaderFunctionId)
expect(clientFunctionName).toBe(loaderFunctionName)
expect(clientFilename).toBe(loaderFilename)

// Client captured ID from client middleware should also match
expect(clientClientCapturedId).toBe(loaderFunctionId)

// Client middleware should NOT have access to name or filename
expect(clientClientCapturedName).toBe('undefined')
expect(clientClientCapturedFilename).toBe('undefined')
})
4 changes: 3 additions & 1 deletion packages/start-client-core/src/client-rpc/createClientRpc.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { TSS_SERVER_FUNCTION } from '../constants'
import type { ClientFnMeta } from '../constants'
import { serverFnFetcher } from './serverFnFetcher'

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

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

return Object.assign(clientFn, {
url,
functionId,
serverFnMeta,
[TSS_SERVER_FUNCTION]: true,
})
}
Loading
Loading