Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createServerFn } from '@tanstack/react-start'

// This function is ONLY called from the server, never directly from client code
export const fnOnlyCalledByServer = createServerFn().handler(() => {
return { message: 'hello from server-only function', secret: 42 }
})
21 changes: 21 additions & 0 deletions e2e/react-start/server-functions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as SubmitPostFormdataRouteImport } from './routes/submit-post-formdata'
import { Route as StatusRouteImport } from './routes/status'
import { Route as ServerOnlyFnRouteImport } from './routes/server-only-fn'
import { Route as SerializeFormDataRouteImport } from './routes/serialize-form-data'
import { Route as ReturnNullRouteImport } from './routes/return-null'
import { Route as RawResponseRouteImport } from './routes/raw-response'
Expand Down Expand Up @@ -49,6 +50,11 @@ const StatusRoute = StatusRouteImport.update({
path: '/status',
getParentRoute: () => rootRouteImport,
} as any)
const ServerOnlyFnRoute = ServerOnlyFnRouteImport.update({
id: '/server-only-fn',
path: '/server-only-fn',
getParentRoute: () => rootRouteImport,
} as any)
const SerializeFormDataRoute = SerializeFormDataRouteImport.update({
id: '/serialize-form-data',
path: '/serialize-form-data',
Expand Down Expand Up @@ -200,6 +206,7 @@ export interface FileRoutesByFullPath {
'/raw-response': typeof RawResponseRoute
'/return-null': typeof ReturnNullRoute
'/serialize-form-data': typeof SerializeFormDataRoute
'/server-only-fn': typeof ServerOnlyFnRoute
'/status': typeof StatusRoute
'/submit-post-formdata': typeof SubmitPostFormdataRoute
'/abort-signal/$method': typeof AbortSignalMethodRoute
Expand Down Expand Up @@ -231,6 +238,7 @@ export interface FileRoutesByTo {
'/raw-response': typeof RawResponseRoute
'/return-null': typeof ReturnNullRoute
'/serialize-form-data': typeof SerializeFormDataRoute
'/server-only-fn': typeof ServerOnlyFnRoute
'/status': typeof StatusRoute
'/submit-post-formdata': typeof SubmitPostFormdataRoute
'/abort-signal/$method': typeof AbortSignalMethodRoute
Expand Down Expand Up @@ -263,6 +271,7 @@ export interface FileRoutesById {
'/raw-response': typeof RawResponseRoute
'/return-null': typeof ReturnNullRoute
'/serialize-form-data': typeof SerializeFormDataRoute
'/server-only-fn': typeof ServerOnlyFnRoute
'/status': typeof StatusRoute
'/submit-post-formdata': typeof SubmitPostFormdataRoute
'/abort-signal/$method': typeof AbortSignalMethodRoute
Expand Down Expand Up @@ -296,6 +305,7 @@ export interface FileRouteTypes {
| '/raw-response'
| '/return-null'
| '/serialize-form-data'
| '/server-only-fn'
| '/status'
| '/submit-post-formdata'
| '/abort-signal/$method'
Expand Down Expand Up @@ -327,6 +337,7 @@ export interface FileRouteTypes {
| '/raw-response'
| '/return-null'
| '/serialize-form-data'
| '/server-only-fn'
| '/status'
| '/submit-post-formdata'
| '/abort-signal/$method'
Expand Down Expand Up @@ -358,6 +369,7 @@ export interface FileRouteTypes {
| '/raw-response'
| '/return-null'
| '/serialize-form-data'
| '/server-only-fn'
| '/status'
| '/submit-post-formdata'
| '/abort-signal/$method'
Expand Down Expand Up @@ -390,6 +402,7 @@ export interface RootRouteChildren {
RawResponseRoute: typeof RawResponseRoute
ReturnNullRoute: typeof ReturnNullRoute
SerializeFormDataRoute: typeof SerializeFormDataRoute
ServerOnlyFnRoute: typeof ServerOnlyFnRoute
StatusRoute: typeof StatusRoute
SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute
AbortSignalMethodRoute: typeof AbortSignalMethodRoute
Expand Down Expand Up @@ -426,6 +439,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof StatusRouteImport
parentRoute: typeof rootRouteImport
}
'/server-only-fn': {
id: '/server-only-fn'
path: '/server-only-fn'
fullPath: '/server-only-fn'
preLoaderRoute: typeof ServerOnlyFnRouteImport
parentRoute: typeof rootRouteImport
}
'/serialize-form-data': {
id: '/serialize-form-data'
path: '/serialize-form-data'
Expand Down Expand Up @@ -630,6 +650,7 @@ const rootRouteChildren: RootRouteChildren = {
RawResponseRoute: RawResponseRoute,
ReturnNullRoute: ReturnNullRoute,
SerializeFormDataRoute: SerializeFormDataRoute,
ServerOnlyFnRoute: ServerOnlyFnRoute,
StatusRoute: StatusRoute,
SubmitPostFormdataRoute: SubmitPostFormdataRoute,
AbortSignalMethodRoute: AbortSignalMethodRoute,
Expand Down
6 changes: 6 additions & 0 deletions e2e/react-start/server-functions/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ function Home() {
<li>
<Link to="/factory">Server Functions Factory E2E tests</Link>
</li>
<li>
<Link to="/server-only-fn">
Server Function only called by Server Environment is kept in the
server build
</Link>
</li>
</ul>
</div>
)
Expand Down
105 changes: 105 additions & 0 deletions e2e/react-start/server-functions/src/routes/server-only-fn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { createFileRoute } from '@tanstack/react-router'
import * as React from 'react'
import { createServerFn } from '@tanstack/react-start'
import { fnOnlyCalledByServer } from '~/functions/fnOnlyCalledByServer'

/**
* This tests that server functions called only from the server (not from the client)
* are still included in the build and work correctly at runtime.
*
* The `fnOnlyCalledByServer` is only called from `proxyFnThatCallsServerOnlyFn` on the server,
* and is never referenced directly from client code.
*/

// This function IS called from the client, and it calls serverOnlyFn on the server
const proxyFnThatCallsServerOnlyFn = createServerFn().handler(async () => {
// Call the server-only function from within another server function
const result = await fnOnlyCalledByServer()
return {
fromServerOnlyFn: result,
wrapper: 'client-callable wrapper',
}
})

const getFnOnlyCalledByServer = createServerFn().handler(async () => {
return fnOnlyCalledByServer
})

export const Route = createFileRoute('/server-only-fn')({
component: ServerOnlyFnTest,
})

function ServerOnlyFnTest() {
const [result, setResult] = React.useState<{
fromServerOnlyFn: { message: string; secret: number }
wrapper: string
} | null>(null)

const [callFromServerResult, setCallFromServerResult] = React.useState<
string | null
>(null)

return (
<div className="p-2 m-2 grid gap-2">
<h3>Server-Only Function Test</h3>
<p>
This tests that server functions which are only called from other server
functions (and never directly from the client) still work correctly.
</p>
<div>
Expected result:{' '}
<code>
<pre data-testid="expected-server-only-fn-result">
{JSON.stringify({
fromServerOnlyFn: {
message: 'hello from server-only function',
secret: 42,
},
wrapper: 'client-callable wrapper',
})}
</pre>
</code>
</div>
<div>
Actual result:{' '}
<code>
<pre data-testid="server-only-fn-result">
{result ? JSON.stringify(result) : 'null'}
</pre>
</code>
</div>
<button
data-testid="test-server-only-fn-btn"
type="button"
className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={async () => {
const res = await proxyFnThatCallsServerOnlyFn()
setResult(res)
}}
>
Test Server-Only Function
</button>

<button
data-testid="call-server-fn-from-client-btn"
className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={async () => {
try {
const fn = await getFnOnlyCalledByServer()
await fn()
setCallFromServerResult('success')
} catch (e) {
setCallFromServerResult('error')
}
}}
>
Call Server Fn From Client
</button>
{callFromServerResult && (
<div data-testid="call-server-fn-from-client-result">
{callFromServerResult}
</div>
)}
</div>
)
}
37 changes: 37 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 @@ -543,3 +543,40 @@ test('redirect in server function called in query during SSR', async ({
await expect(page.getByTestId('redirect-target-ssr')).toBeVisible()
expect(page.url()).toContain('/redirect-test-ssr/target')
})

test('server function called only from server (not client) works correctly', async ({
page,
}) => {
await page.goto('/server-only-fn')

await page.waitForLoadState('networkidle')

const expected =
(await page.getByTestId('expected-server-only-fn-result').textContent()) ||
''
expect(expected).not.toBe('')

await page.getByTestId('test-server-only-fn-btn').click()
await page.waitForLoadState('networkidle')

await expect(page.getByTestId('server-only-fn-result')).toContainText(
expected,
)
})

test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 500/,
],
})
test('server function called only from server (not client) cannot be called from the client', async ({
page,
}) => {
await page.goto('/server-only-fn')
await page.waitForLoadState('networkidle')

await page.getByTestId('call-server-fn-from-client-btn').click()
await expect(
page.getByTestId('call-server-fn-from-client-result'),
).toContainText('error')
})
Loading
Loading