Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 10 additions & 16 deletions docs/start/framework/react/guide/server-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,18 @@ Cache server function results at build time for static generation. See [Static S

Handle request cancellation with `AbortSignal` for long-running operations.

### Function ID generation
### Function ID generation for production build

Server functions are addressed by a generated, stable function ID under the hood. These IDs are embedded into the client/SSR builds and used by the server to locate and import the correct module at runtime.

Defaults:

- In development, IDs are URL-safe strings derived from `${filename}--${functionName}` to aid debugging.
- In production, IDs are SHA256 hashes of the same seed to keep bundles compact and avoid leaking file paths.
- If two server functions end up with the same ID (including when using a custom generator), the system de-duplicates by appending an incrementing suffix like `_1`, `_2`, etc.
- IDs are stable for a given file/function tuple for the lifetime of the process (hot updates keep the same mapping).
By default, IDs are SHA256 hashes of the same seed to keep bundles compact and avoid leaking file paths.
If two server functions end up with the same ID (including when using a custom generator), the system de-duplicates by appending an incrementing suffix like `_1`, `_2`, etc.

Customization:

You can customize function ID generation by providing a `generateFunctionId` function when configuring the TanStack Start Vite plugin.
You can customize function ID generation for the production build by providing a `generateFunctionId` function when configuring the TanStack Start Vite plugin.

Prefer deterministic inputs (filename + functionName) so IDs remain stable between builds.

Please note that this customization is **experimental** und subject to change.

Expand All @@ -259,8 +257,10 @@ export default defineConfig({
serverFns: {
generateFunctionId: ({ filename, functionName }) => {
// Return a custom ID string. If you return undefined, the default is used.
// For example, always hash (even in dev):
// return createHash('sha256').update(`${filename}--${functionName}`).digest('hex')
return crypto
.createHash('sha1')
.update(`${filename}--${functionName}`)
.digest('hex')
return undefined
Comment on lines 258 to 264
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix unreachable code and hash algorithm inconsistency.

Two issues:

  1. Unreachable code: Line 264 has return undefined after line 263 already returns the hash, making it unreachable.
  2. Hash algorithm mismatch: Line 235 states defaults use "SHA256 hashes", but the example uses sha1 (line 261).

Apply this diff:

         generateFunctionId: ({ filename, functionName }) => {
           // Return a custom ID string. If you return undefined, the default is used.
-          return crypto
-            .createHash('sha1')
+          return crypto
+            .createHash('sha256')
             .update(`${filename}--${functionName}`)
             .digest('hex')
-          return undefined
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
generateFunctionId: ({ filename, functionName }) => {
// Return a custom ID string. If you return undefined, the default is used.
// For example, always hash (even in dev):
// return createHash('sha256').update(`${filename}--${functionName}`).digest('hex')
return crypto
.createHash('sha1')
.update(`${filename}--${functionName}`)
.digest('hex')
return undefined
generateFunctionId: ({ filename, functionName }) => {
// Return a custom ID string. If you return undefined, the default is used.
return crypto
.createHash('sha256')
.update(`${filename}--${functionName}`)
.digest('hex')
},
🤖 Prompt for AI Agents
In docs/start/framework/react/guide/server-functions.md around lines 258 to 264,
the example for generateFunctionId contains an unreachable `return undefined`
after returning a hash and uses `sha1` while the surrounding text says SHA256;
remove the trailing `return undefined` and replace `sha1` with `sha256` so the
function consistently returns a SHA-256 hex digest (or if you intended to allow
undefined, make the return conditional instead).

},
},
Expand All @@ -270,12 +270,6 @@ export default defineConfig({
})
```

Tips:

- Prefer deterministic inputs (filename + functionName) so IDs remain stable between builds.
- If you don’t want file paths in dev IDs, return a hash in all environments.
- Ensure the returned ID is **URL-safe**.

---

> **Note**: Server functions use a compilation process that extracts server code from client bundles while maintaining seamless calling patterns. On the client, calls become `fetch` requests to the server.
5 changes: 3 additions & 2 deletions packages/directive-functions-plugin/src/compilers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type SupportedFunctionPath =
export type GenerateFunctionIdFn = (opts: {
filename: string
functionName: string
extractedFilename: string
}) => string

Comment on lines 26 to 31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Threading extractedFilename into ID generation looks good; verify call sites updated.

The new GenerateFunctionIdFn signature adds extractedFilename. Ensure all implementations and usages across the repo are updated to avoid type drift.

Run to locate and validate implementors:


🏁 Script executed:

#!/bin/bash
# Find typedef and implementors/usages
rg -nP --type=ts 'type\s+GenerateFunctionIdFn\b|interface\s+GenerateFunctionIdFn\b'
rg -nP --type=ts '\bgenerateFunctionId\s*:\s*\(' -C3
rg -nP --type=ts '\bgenerateFunctionId\(\s*\{[^}]*\}\s*\)' -C2

Length of output: 712



🏁 Script executed:

#!/bin/bash
# Find all generateFunctionId call sites and implementations more broadly
echo "=== All generateFunctionId property definitions ==="
rg -nP --type=ts 'generateFunctionId\s*[:=]' -C5

echo ""
echo "=== All places where generateFunctionId is called ==="
rg -nP --type=ts 'generateFunctionId\s*\(' -C3

echo ""
echo "=== Files containing generateFunctionId ==="
fd --type f -e ts -e tsx | xargs rg -l 'generateFunctionId'

Length of output: 9970


🏁 Script executed:

#!/bin/bash
# Check the schema definition for generateFunctionId
echo "=== Schema definition for generateFunctionId ==="
sed -n '162,175p' packages/start-plugin-core/src/schema.ts

echo ""
echo "=== Full call site in server-functions-plugin ==="
sed -n '90,102p' packages/server-functions-plugin/src/index.ts

echo ""
echo "=== Full call site in directive-functions-plugin ==="
sed -n '490,500p' packages/directive-functions-plugin/src/compilers.ts

Length of output: 1387


Type signature mismatch: GenerateFunctionIdFn requires extractedFilename but not all call sites provide it.

The reviewed change adds extractedFilename as a required parameter to GenerateFunctionIdFn, but this creates a type safety violation:

  • directive-functions-plugin (compilers.ts:494) passes {filename, functionName, extractedFilename}
  • server-functions-plugin (index.ts:95) passes only {filename, functionName}
  • Schema (start-plugin-core/src/schema.ts:162) defines args as {filename, functionName} only—does not include extractedFilename

In strict TypeScript mode, the server-functions-plugin call will fail type checking. Either:

  1. Make extractedFilename optional in GenerateFunctionIdFn and update the schema accordingly, or
  2. Update server-functions-plugin to pass extractedFilename and update the schema to include it
🤖 Prompt for AI Agents
In packages/directive-functions-plugin/src/compilers.ts around lines 26–31, the
new GenerateFunctionIdFn requires extractedFilename but other call sites and the
schema only provide filename and functionName; make extractedFilename optional
on the GenerateFunctionIdFn type (e.g., extractedFilename?: string) and update
any related schema/type definitions (start-plugin-core/src/schema.ts) to declare
extractedFilename as optional too, then adjust callers (server-functions-plugin
index.ts and any others) to either pass extractedFilename when available or rely
on the optional property so TypeScript strict mode typechecks successfully.

export type ReplacerFn = (opts: {
Expand All @@ -46,7 +47,6 @@ export type CompileDirectivesOpts = ParseAstOptions & {
}) => string
generateFunctionId: GenerateFunctionIdFn
replacer: ReplacerFn
// devSplitImporter: string
filename: string
root: string
}
Expand Down Expand Up @@ -493,7 +493,8 @@ export function findDirectives(
const relativeFilename = path.relative(opts.root, baseFilename)
const functionId = opts.generateFunctionId({
filename: relativeFilename,
functionName: functionName,
functionName,
extractedFilename,
})
// If a replacer is provided, replace the function with the replacer
if (opts.replacer) {
Expand Down
26 changes: 0 additions & 26 deletions packages/directive-functions-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,6 @@ export type DirectiveFunctionsViteOptions = Pick<
const createDirectiveRx = (directive: string) =>
new RegExp(`"${directive}"|'${directive}'`, 'gm')

export function TanStackDirectiveFunctionsPlugin(
opts: DirectiveFunctionsViteOptions,
): Plugin {
let root: string = process.cwd()

const directiveRx = createDirectiveRx(opts.directive)

return {
name: 'tanstack-start-directive-vite-plugin',
enforce: 'pre',
configResolved: (config) => {
root = config.root
},
transform(code, id) {
return transformCode({ ...opts, code, id, directiveRx, root })
},
}
}

export type DirectiveFunctionsVitePluginEnvOptions = Pick<
CompileDirectivesOpts,
'directive' | 'directiveLabel'
Expand Down Expand Up @@ -122,7 +103,6 @@ export function TanStackDirectiveFunctionsPluginEnv(
...envOptions,
code,
id,
directiveRx,
root,
})
},
Expand All @@ -133,7 +113,6 @@ export function TanStackDirectiveFunctionsPluginEnv(
function transformCode({
code,
id,
directiveRx,
envLabel,
directive,
directiveLabel,
Expand All @@ -145,17 +124,12 @@ function transformCode({
}: DirectiveFunctionsViteOptions & {
code: string
id: string
directiveRx: RegExp
root: string
}) {
const url = pathToFileURL(id)
url.searchParams.delete('v')
id = fileURLToPath(url).replace(/\\/g, '/')

if (!code.match(directiveRx)) {
return null
}

if (debug) console.info(`${envLabel}: Compiling Directives: `, id)

const { compiledResult, directiveFnsById, isDirectiveSplitParam } =
Expand Down
45 changes: 15 additions & 30 deletions packages/server-functions-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,29 @@
Create a new instance of the plugin with the following options:

```ts
const TanStackServerFnsPlugin = createTanStackServerFnPlugin({
// This is the ID (virtual module) that will be made available to look up
// and import our server function manifest and resolve its modules.
TanStackServerFnPlugin({
// This is the ID that will be available to look up and import
// our server function manifest and resolve its module
manifestVirtualImportId: 'tanstack:server-fn-manifest',
generateFunctionId: startPluginOpts?.serverFns?.generateFunctionId,
client: {
getRuntimeCode: () =>
`import { createClientRpc } from '@tanstack/react-start/client-runtime'`,
replacer: (opts) => `createClientRpc(${JSON.stringify(opts.functionId)})`,
},
ssr: {
getRuntimeCode: () =>
`import { createSsrRpc } from '@tanstack/react-start/ssr-runtime'`,
replacer: (opts) => `createSsrRpc(${JSON.stringify(opts.functionId)})`,
`import { createClientRpc } from '@tanstack/${corePluginOpts.framework}-start/client-rpc'`,
replacer: (d) => `createClientRpc('${d.functionId}')`,
envName: 'client',
},
server: {
getRuntimeCode: () =>
`import { createServerRpc } from '@tanstack/react-start/server-runtime'`,
replacer: (opts) =>
`createServerRpc(${JSON.stringify(opts.functionId)}, ${opts.fn})`,
`import { createServerRpc } from '@tanstack/${corePluginOpts.framework}-start/server-rpc'`,
replacer: (d) => `createServerRpc('${d.functionId}', ${d.fn})`,
envName: 'ssr',
},
Comment on lines 21 to 26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify envName value for server configuration.

The server configuration has envName: 'ssr' (line 25), which seems inconsistent. Should this be 'server' instead? The client has envName: 'client', so having the server marked as 'ssr' appears counterintuitive.


🏁 Script executed:

#!/bin/bash
# Search for envName usage patterns in server-functions-plugin and related packages
rg -nP --type=ts "envName:\s*['\"]" -A2 -B2

Length of output: 665


I found that directive-functions-plugin uses envName: 'server' for the server configuration. Let me verify what the actual implementation in server-functions-plugin uses:



🏁 Script executed:

#!/bin/bash
# Search for envName in server-functions-plugin source code (not README)
rg -n "envName" packages/server-functions-plugin/src --type=ts

Length of output: 816


🏁 Script executed:

#!/bin/bash
# Also check the actual server-functions-plugin README to see full context
head -30 packages/server-functions-plugin/README.md

Length of output: 990


Update README.md to use correct envName value for server configuration.

The README documentation incorrectly shows envName: 'ssr' on line 25, but the actual implementation defaults to envName: 'server' (source: src/index.ts:48). This inconsistency should be fixed in the README to match the implementation and maintain consistency with the client configuration pattern (envName: 'client').

Update line 25 from:

    envName: 'ssr',

to:

    envName: 'server',
🤖 Prompt for AI Agents
In packages/server-functions-plugin/README.md around lines 21 to 26, the README
shows envName: 'ssr' which is inconsistent with the implementation
(src/index.ts:48) that defaults to 'server'; update the README to use envName:
'server' so it matches the code and the client pattern.

})
```

Then you can inject the plugin into the appropriate vite config plugin arrays:

```ts
clientVitePlugins: [TanStackServerFnsPlugin.client]
ssrVitePlugins: [TanStackServerFnsPlugin.ssr]
serverVitePlugins: [TanStackServerFnsPlugin.server]
}),
```

## Providing the wrapper implementations

Each runtime replacement should be implemented by your framework. Generally, on the client and SSR runtimes, you'll end up using a `fetch` call to call the server function your desired endpoint, like this:
Each runtime replacement should be implemented by your framework. Generally, on the client runtime, you'll end up using a `fetch` call to call the server function your desired endpoint, like this:

```ts
function createClientRpc(functionId: string) {
Expand Down Expand Up @@ -71,20 +60,16 @@ function createClientRpc(functionId: string) {
In your server handler, you can import the manifest and use it to look up and dynamically import the server function you want to call.

```ts
import serverFnManifest from 'tanstack:server-fn-manifest'
import { getServerFnById } from 'tanstack:server-fn-manifest'

export const handler = async (req: Request) => {
const functionId = req.url.split('/').pop()
invariant(functionId, 'No function ID provided')

const fnInfo = serverFnManifest[functionId]
invariant(fn, `Server function ${functionId} not found`)

const fnModule = await fnInfo.importer()
invariant(fnModule, `Server function ${functionId} could not be imported`)
const serverFn = await getServerFnById(functionId)

const args = await req.json()

return await fnModule(...args)
return await serverFn(...args)
}
```
Loading
Loading