From 9302a861179b78f2e37343e7a366f750de1503da Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Sun, 5 Oct 2025 18:29:44 +0300 Subject: [PATCH 01/12] Add support for customizable `functionId` generation. If not provided, the default (basename_functionName) will be used in development and sha256 of the default for production --- .../directive-functions-plugin/package.json | 1 + .../src/compilers.ts | 33 ++++++++-- .../directive-functions-plugin/src/index.ts | 11 +++- packages/server-functions-plugin/src/index.ts | 62 +++++++++++++------ packages/start-plugin-core/src/plugin.ts | 1 + packages/start-plugin-core/src/schema.ts | 9 +++ pnpm-lock.yaml | 3 + 7 files changed, 96 insertions(+), 24 deletions(-) diff --git a/packages/directive-functions-plugin/package.json b/packages/directive-functions-plugin/package.json index a1a44e45bcd..427118a18a9 100644 --- a/packages/directive-functions-plugin/package.json +++ b/packages/directive-functions-plugin/package.json @@ -71,6 +71,7 @@ "@babel/types": "^7.27.7", "@tanstack/router-utils": "workspace:*", "babel-dead-code-elimination": "^1.0.10", + "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "devDependencies": { diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index 0bb79020272..07d61c7e699 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -5,6 +5,7 @@ import { deadCodeElimination, findReferencedIdentifiers, } from 'babel-dead-code-elimination' +import path from 'pathe' import { generateFromAst, parseAst } from '@tanstack/router-utils' import type { GeneratorResult, ParseAstOptions } from '@tanstack/router-utils' @@ -22,6 +23,8 @@ export type SupportedFunctionPath = | babel.NodePath | babel.NodePath +export type FunctionIdFn = (opts: { currentId: string }) => string + export type ReplacerFn = (opts: { fn: string extractedFilename: string @@ -38,6 +41,7 @@ export type CompileDirectivesOpts = ParseAstOptions & { getRuntimeCode?: (opts: { directiveFnsById: Record }) => string + functionId?: FunctionIdFn replacer: ReplacerFn // devSplitImporter: string filename: string @@ -198,7 +202,7 @@ function findNearestVariableName( return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' } -function makeFileLocationUrlSafe(location: string): string { +function makeFunctionIdUrlSafe(location: string): string { return location .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore .replace(/_{2,}/g, '_') // Collapse multiple underscores @@ -221,6 +225,7 @@ export function findDirectives( directive: string directiveLabel: string replacer?: ReplacerFn + functionId?: FunctionIdFn directiveSplitParam: string filename: string root: string @@ -460,15 +465,33 @@ export function findDirectives( `body.${topParentIndex}.declarations.0.init`, ) as SupportedFunctionPath - const [baseFilename, ..._searchParams] = opts.filename.split('?') + const [baseFilename, ..._searchParams] = opts.filename.split('?') as [ + string, + ...Array, + ] const searchParams = new URLSearchParams(_searchParams.join('&')) searchParams.set(opts.directiveSplitParam, '') const extractedFilename = `${baseFilename}?${searchParams.toString()}` - const functionId = makeFileLocationUrlSafe( - `${baseFilename}--${functionName}`.replace(opts.root, ''), - ) + // Relative in order to have constant functionId regardless of the machine + // that we are executing + const relativeFilename = path.relative(opts.root, baseFilename) + let functionId = `${relativeFilename}--${functionName}` + if (opts.functionId) { + functionId = opts.functionId({ currentId: functionId }) + // Handle cases in which the returned id conflicts with + // one of the already defined ids + if (functionId in directiveFnsById) { + let deduplicatedId = functionId + let iteration = 0 + do { + deduplicatedId = `${deduplicatedId}_${++iteration}` + } while (deduplicatedId in directiveFnsById) + functionId = deduplicatedId + } + } + functionId = makeFunctionIdUrlSafe(functionId) // If a replacer is provided, replace the function with the replacer if (opts.replacer) { diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts index 4e05962e948..51b2f01a8c3 100644 --- a/packages/directive-functions-plugin/src/index.ts +++ b/packages/directive-functions-plugin/src/index.ts @@ -2,7 +2,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { logDiff } from '@tanstack/router-utils' import { compileDirectives } from './compilers' -import type { CompileDirectivesOpts, DirectiveFn } from './compilers' +import type { + CompileDirectivesOpts, + DirectiveFn, + FunctionIdFn, +} from './compilers' import type { Plugin } from 'vite' const debug = @@ -13,6 +17,7 @@ export type { DirectiveFn, CompileDirectivesOpts, ReplacerFn, + FunctionIdFn, } from './compilers' export type DirectiveFunctionsViteEnvOptions = Pick< @@ -28,6 +33,7 @@ export type DirectiveFunctionsViteOptions = Pick< > & DirectiveFunctionsViteEnvOptions & { onDirectiveFnsById?: (directiveFnsById: Record) => void + functionId?: FunctionIdFn } const createDirectiveRx = (directive: string) => @@ -61,6 +67,7 @@ export type DirectiveFunctionsVitePluginEnvOptions = Pick< server: DirectiveFunctionsViteEnvOptions & { envName?: string } } onDirectiveFnsById?: (directiveFnsById: Record) => void + functionId?: FunctionIdFn } export function TanStackDirectiveFunctionsPluginEnv( @@ -131,6 +138,7 @@ function transformCode({ directive, directiveLabel, getRuntimeCode, + functionId, replacer, onDirectiveFnsById, root, @@ -155,6 +163,7 @@ function transformCode({ directive, directiveLabel, getRuntimeCode, + functionId, replacer, code, root, diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index 710fe0b2ba2..c68a9b0b90d 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -1,3 +1,4 @@ +import crypto from 'node:crypto' import { TanStackDirectiveFunctionsPlugin, TanStackDirectiveFunctionsPluginEnv, @@ -5,6 +6,7 @@ import { import type { DevEnvironment, Plugin, ViteDevServer } from 'vite' import type { DirectiveFn, + FunctionIdFn, ReplacerFn, } from '@tanstack/directive-functions-plugin' @@ -17,6 +19,7 @@ export type ServerFnPluginOpts = { * and its modules. */ manifestVirtualImportId: string + functionId?: FunctionIdFn client: ServerFnPluginEnvOpts ssr: ServerFnPluginEnvOpts server: ServerFnPluginEnvOpts @@ -25,6 +28,7 @@ export type ServerFnPluginOpts = { export type ServerFnPluginEnvOpts = { getRuntimeCode: () => string replacer: ReplacerFn + envName?: string } const debug = @@ -54,6 +58,12 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { } }, }) + const functionId = buildFunctionId({ + functionId: opts.functionId + ? opts.functionId + : opts => opts.currentId, + directiveFnsById, + }) const directive = 'use server' const directiveLabel = 'Server Function' @@ -67,6 +77,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { directive, directiveLabel, getRuntimeCode: opts.client.getRuntimeCode, + functionId, replacer: opts.client.replacer, onDirectiveFnsById, }), @@ -78,6 +89,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { directive, directiveLabel, getRuntimeCode: opts.ssr.getRuntimeCode, + functionId, replacer: opts.ssr.replacer, onDirectiveFnsById, }), @@ -127,6 +139,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { directive, directiveLabel, getRuntimeCode: opts.server.getRuntimeCode, + functionId, replacer: opts.server.replacer, onDirectiveFnsById, }), @@ -134,24 +147,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { } } -export interface TanStackServerFnPluginEnvOpts { - /** - * The virtual import ID that will be used to import the server function manifest. - * This virtual import ID will be used in the server build to import the manifest - * and its modules. - */ - manifestVirtualImportId: string - client: { - envName?: string - getRuntimeCode: () => string - replacer: ReplacerFn - } - server: { - envName?: string - getRuntimeCode: () => string - replacer: ReplacerFn - } -} +export type TanStackServerFnPluginEnvOpts = Omit export function TanStackServerFnPluginEnv( _opts: TanStackServerFnPluginEnvOpts, @@ -188,6 +184,17 @@ export function TanStackServerFnPluginEnv( } }, }) + const functionId = buildFunctionId({ + functionId: (functionIdOpts) => { + // If the consumer provided a functionId then use that for all cases. + // If not then return the currentId on development + // and SHA256 using the currentId as seed on production + if (opts.functionId) return opts.functionId(functionIdOpts) + else if (serverDevEnv) return functionIdOpts.currentId + else return crypto.createHash('sha256').update(functionIdOpts.currentId).digest('hex') + }, + directiveFnsById, + }) const directive = 'use server' const directiveLabel = 'Server Function' @@ -199,6 +206,7 @@ export function TanStackServerFnPluginEnv( directive, directiveLabel, onDirectiveFnsById, + functionId, environments: { client: { envLabel: 'Client', @@ -262,6 +270,24 @@ function resolveViteId(id: string) { return `\0${id}` } +function buildFunctionId(opts: { + functionId: FunctionIdFn + directiveFnsById: Record +}): FunctionIdFn { + return (functionIdOps) => { + let generatedId = opts.functionId(functionIdOps) + if (generatedId in opts.directiveFnsById) { + let deduplicatedId = generatedId + let iteration = 0 + do { + deduplicatedId = `${deduplicatedId}_${++iteration}` + } while (deduplicatedId in opts.directiveFnsById) + generatedId = deduplicatedId + } + return generatedId + } +} + function buildOnDirectiveFnsByIdCallback(opts: { invalidateModule: (resolvedId: string) => void directiveFnsById: Record diff --git a/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts index 070c82731b1..cb0a5ded33a 100644 --- a/packages/start-plugin-core/src/plugin.ts +++ b/packages/start-plugin-core/src/plugin.ts @@ -334,6 +334,7 @@ export function TanStackStartVitePluginCore( // This is the ID that will be available to look up and import // our server function manifest and resolve its module manifestVirtualImportId: VIRTUAL_MODULES.serverFnManifest, + functionId: startPluginOpts?.serverFns?.functionId, client: { getRuntimeCode: () => `import { createClientRpc } from '@tanstack/${corePluginOpts.framework}-start/client-rpc'`, diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index 2c0b3b6d9ae..3bf80d72d0e 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -159,6 +159,15 @@ const tanstackStartOptionsSchema = z serverFns: z .object({ base: z.string().optional().default('/_serverFn'), + functionId: z + .function() + .args( + z.object({ + currentId: z.string(), + }), + ) + .returns(z.string()) + .optional(), }) .optional() .default({}), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 303cefff7b6..4231a759934 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6643,6 +6643,9 @@ importers: babel-dead-code-elimination: specifier: ^1.0.10 version: 1.0.10 + pathe: + specifier: ^2.0.3 + version: 2.0.3 tiny-invariant: specifier: ^1.3.3 version: 1.3.3 From bb6914da9792dafcd1b46f805829f2c3519026c4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 15:31:40 +0000 Subject: [PATCH 02/12] ci: apply automated fixes --- packages/server-functions-plugin/src/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index c68a9b0b90d..7940adbadc0 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -59,9 +59,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { }, }) const functionId = buildFunctionId({ - functionId: opts.functionId - ? opts.functionId - : opts => opts.currentId, + functionId: opts.functionId ? opts.functionId : (opts) => opts.currentId, directiveFnsById, }) @@ -191,7 +189,11 @@ export function TanStackServerFnPluginEnv( // and SHA256 using the currentId as seed on production if (opts.functionId) return opts.functionId(functionIdOpts) else if (serverDevEnv) return functionIdOpts.currentId - else return crypto.createHash('sha256').update(functionIdOpts.currentId).digest('hex') + else + return crypto + .createHash('sha256') + .update(functionIdOpts.currentId) + .digest('hex') }, directiveFnsById, }) From e3b9ef9a871f83b84ba9d3c9e7e1f9df1de50399 Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Sun, 5 Oct 2025 19:59:35 +0300 Subject: [PATCH 03/12] Fixed failing tests --- .../custom-basepath/tests/navigation.spec.ts | 4 +- packages/server-functions-plugin/src/index.ts | 60 +++++++++---------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/e2e/react-start/custom-basepath/tests/navigation.spec.ts b/e2e/react-start/custom-basepath/tests/navigation.spec.ts index 765609c47de..c96cc384c88 100644 --- a/e2e/react-start/custom-basepath/tests/navigation.spec.ts +++ b/e2e/react-start/custom-basepath/tests/navigation.spec.ts @@ -41,9 +41,7 @@ test('Server function URLs correctly include app basepath', async ({ const form = page.locator('form') const actionUrl = await form.getAttribute('action') - expect(actionUrl).toBe( - '/custom/basepath/_serverFn/src_routes_logout_tsx--logoutFn_createServerFn_handler', - ) + expect(actionUrl).toMatch(/^\/custom\/basepath\/_serverFn\//) }) test('client-side redirect', async ({ page, baseURL }) => { diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index 7940adbadc0..0ab4e60784a 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -58,10 +58,9 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { } }, }) - const functionId = buildFunctionId({ - functionId: opts.functionId ? opts.functionId : (opts) => opts.currentId, - directiveFnsById, - }) + const functionId = buildFunctionId( + opts.functionId ? opts.functionId : (opts) => opts.currentId, + ) const directive = 'use server' const directiveLabel = 'Server Function' @@ -182,20 +181,17 @@ export function TanStackServerFnPluginEnv( } }, }) - const functionId = buildFunctionId({ - functionId: (functionIdOpts) => { - // If the consumer provided a functionId then use that for all cases. - // If not then return the currentId on development - // and SHA256 using the currentId as seed on production - if (opts.functionId) return opts.functionId(functionIdOpts) - else if (serverDevEnv) return functionIdOpts.currentId - else - return crypto - .createHash('sha256') - .update(functionIdOpts.currentId) - .digest('hex') - }, - directiveFnsById, + const functionId = buildFunctionId((functionIdOpts) => { + // If the consumer provided a functionId then use that for all cases. + // If not, then return the currentId on development + // and SHA256 using the currentId as seed on production + if (opts.functionId) return opts.functionId(functionIdOpts) + else if (serverDevEnv) return functionIdOpts.currentId + else + return crypto + .createHash('sha256') + .update(functionIdOpts.currentId) + .digest('hex') }) const directive = 'use server' @@ -272,19 +268,23 @@ function resolveViteId(id: string) { return `\0${id}` } -function buildFunctionId(opts: { - functionId: FunctionIdFn - directiveFnsById: Record -}): FunctionIdFn { +function buildFunctionId(delegate: FunctionIdFn): FunctionIdFn { + const cache = new Map() return (functionIdOps) => { - let generatedId = opts.functionId(functionIdOps) - if (generatedId in opts.directiveFnsById) { - let deduplicatedId = generatedId - let iteration = 0 - do { - deduplicatedId = `${deduplicatedId}_${++iteration}` - } while (deduplicatedId in opts.directiveFnsById) - generatedId = deduplicatedId + // Keep the previous id in case we already generated it. This is for consistency + // between client / server builds and hot reload + let generatedId = cache.get(functionIdOps.currentId) + if (generatedId === undefined) { + generatedId = delegate(functionIdOps) + if (cache.has(generatedId)) { + let deduplicatedId = generatedId + let iteration = 0 + do { + deduplicatedId = `${deduplicatedId}_${++iteration}` + } while (cache.has(deduplicatedId)) + generatedId = deduplicatedId + } + cache.set(functionIdOps.currentId, generatedId) } return generatedId } From 800d4db7e8330fc53a422566c10cbad4af1d6ff2 Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Sun, 5 Oct 2025 21:07:48 +0300 Subject: [PATCH 04/12] Refactor `functionId` handling for better flexibility and deduplication. Add support for an optional `undefined` return and streamline generation process. --- .../src/compilers.ts | 10 +-- packages/server-functions-plugin/src/index.ts | 61 +++++++++++-------- packages/start-plugin-core/src/schema.ts | 2 +- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index 07d61c7e699..6376b44a86d 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -23,7 +23,7 @@ export type SupportedFunctionPath = | babel.NodePath | babel.NodePath -export type FunctionIdFn = (opts: { currentId: string }) => string +export type FunctionIdFn = (opts: { currentId: string }) => string | undefined export type ReplacerFn = (opts: { fn: string @@ -474,19 +474,19 @@ export function findDirectives( const extractedFilename = `${baseFilename}?${searchParams.toString()}` - // Relative in order to have constant functionId regardless of the machine + // Relative to have constant functionId regardless of the machine // that we are executing const relativeFilename = path.relative(opts.root, baseFilename) let functionId = `${relativeFilename}--${functionName}` if (opts.functionId) { - functionId = opts.functionId({ currentId: functionId }) + functionId = opts.functionId({ currentId: functionId }) ?? functionId // Handle cases in which the returned id conflicts with // one of the already defined ids if (functionId in directiveFnsById) { - let deduplicatedId = functionId + let deduplicatedId let iteration = 0 do { - deduplicatedId = `${deduplicatedId}_${++iteration}` + deduplicatedId = `${functionId}_${++iteration}` } while (deduplicatedId in directiveFnsById) functionId = deduplicatedId } diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index 0ab4e60784a..e23e4b54ff1 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -58,8 +58,8 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { } }, }) - const functionId = buildFunctionId( - opts.functionId ? opts.functionId : (opts) => opts.currentId, + const functionId = buildFunctionId((functionIdOpts, next) => + next(Boolean(viteDevServer), opts.functionId?.(functionIdOpts)), ) const directive = 'use server' @@ -181,18 +181,10 @@ export function TanStackServerFnPluginEnv( } }, }) - const functionId = buildFunctionId((functionIdOpts) => { - // If the consumer provided a functionId then use that for all cases. - // If not, then return the currentId on development - // and SHA256 using the currentId as seed on production - if (opts.functionId) return opts.functionId(functionIdOpts) - else if (serverDevEnv) return functionIdOpts.currentId - else - return crypto - .createHash('sha256') - .update(functionIdOpts.currentId) - .digest('hex') - }) + + const functionId = buildFunctionId((functionIdOpts, next) => + next(Boolean(serverDevEnv), opts.functionId?.(functionIdOpts)), + ) const directive = 'use server' const directiveLabel = 'Server Function' @@ -268,23 +260,44 @@ function resolveViteId(id: string) { return `\0${id}` } -function buildFunctionId(delegate: FunctionIdFn): FunctionIdFn { - const cache = new Map() - return (functionIdOps) => { +function buildFunctionId( + delegate: ( + opts: Parameters[0], + next: (dev: boolean, value?: string) => string, + ) => string, +): FunctionIdFn { + const currentIdToGeneratedId = new Map() + const generatedIds = new Set() + return (opts) => { // Keep the previous id in case we already generated it. This is for consistency // between client / server builds and hot reload - let generatedId = cache.get(functionIdOps.currentId) + let generatedId = currentIdToGeneratedId.get(opts.currentId) if (generatedId === undefined) { - generatedId = delegate(functionIdOps) - if (cache.has(generatedId)) { - let deduplicatedId = generatedId + generatedId = delegate(opts, (dev, newId) => { + // If no value provided, then return the currentId on development + // and SHA256 using the currentId as seed on production + if (newId === undefined) { + if (dev) newId = opts.currentId + else + newId = crypto + .createHash('sha256') + .update(opts.currentId) + .digest('hex') + } + return newId + }) + + // Deduplicate in case the generated id conflicts with an existing id + if (generatedIds.has(generatedId)) { + let deduplicatedId let iteration = 0 do { - deduplicatedId = `${deduplicatedId}_${++iteration}` - } while (cache.has(deduplicatedId)) + deduplicatedId = `${generatedId}_${++iteration}` + } while (generatedIds.has(deduplicatedId)) generatedId = deduplicatedId } - cache.set(functionIdOps.currentId, generatedId) + currentIdToGeneratedId.set(opts.currentId, generatedId) + generatedIds.add(generatedId) } return generatedId } diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index 3bf80d72d0e..8b94a126b39 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -166,7 +166,7 @@ const tanstackStartOptionsSchema = z currentId: z.string(), }), ) - .returns(z.string()) + .returns(z.string().optional()) .optional(), }) .optional() From f5d22d1967c873b353e343429e443f86dd4d604c Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Sun, 5 Oct 2025 21:07:58 +0300 Subject: [PATCH 05/12] Add tests for constant `functionId` and update `serverFns` config logic. --- .../tests/server-functions.spec.ts | 14 ++++++++++++++ e2e/react-start/server-functions/vite.config.ts | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts index 990487289fd..72488ad0dc1 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -6,6 +6,20 @@ import type { Page } from '@playwright/test' const PORT = await getTestServerPort(packageJson.name) +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, }) => { diff --git a/e2e/react-start/server-functions/vite.config.ts b/e2e/react-start/server-functions/vite.config.ts index dc57f144e4f..236559775ca 100644 --- a/e2e/react-start/server-functions/vite.config.ts +++ b/e2e/react-start/server-functions/vite.config.ts @@ -3,12 +3,25 @@ import tsConfigPaths from 'vite-tsconfig-paths' import { tanstackStart } from '@tanstack/react-start/plugin/vite' import viteReact from '@vitejs/plugin-react' +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({ plugins: [ tsConfigPaths({ projects: ['./tsconfig.json'], }), - tanstackStart(), + tanstackStart({ + serverFns: { + functionId: (opts) => { + if (FUNCTIONS_WITH_CONSTANT_ID.includes(opts.currentId)) + return 'constant_id' + else return undefined + }, + }, + }), viteReact(), ], }) From 391aa5f030c7cb899e7dde2614e2be236239735f Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Sun, 5 Oct 2025 21:54:45 +0300 Subject: [PATCH 06/12] Rename `functionId` to `generateFunctionId` for clarity and consistency across packages. Adjust associated logic and type definitions. Only the internal implementation converts to url safe. If a consumer wants to use their own generator, the responsibility of generating safe urls is on them --- .../src/compilers.ts | 22 ++++----- .../directive-functions-plugin/src/index.ts | 12 ++--- packages/server-functions-plugin/src/index.ts | 47 ++++++++++++------- packages/start-plugin-core/src/plugin.ts | 2 +- packages/start-plugin-core/src/schema.ts | 2 +- 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index 6376b44a86d..a7d8fc47df0 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -23,7 +23,9 @@ export type SupportedFunctionPath = | babel.NodePath | babel.NodePath -export type FunctionIdFn = (opts: { currentId: string }) => string | undefined +export type GenerateFunctionIdFn = (opts: { + currentId: string +}) => string | undefined export type ReplacerFn = (opts: { fn: string @@ -41,7 +43,7 @@ export type CompileDirectivesOpts = ParseAstOptions & { getRuntimeCode?: (opts: { directiveFnsById: Record }) => string - functionId?: FunctionIdFn + generateFunctionId?: GenerateFunctionIdFn replacer: ReplacerFn // devSplitImporter: string filename: string @@ -202,14 +204,6 @@ function findNearestVariableName( return nameParts.length > 0 ? nameParts.join('_') : 'anonymous' } -function makeFunctionIdUrlSafe(location: string): string { - return location - .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore - .replace(/_{2,}/g, '_') // Collapse multiple underscores - .replace(/^_|_$/g, '') // Trim leading/trailing underscores - .replace(/_--/g, '--') // Clean up the joiner -} - function makeIdentifierSafe(identifier: string): string { return identifier .replace(/[^a-zA-Z0-9_$]/g, '_') // Replace unsafe chars with underscore @@ -225,7 +219,7 @@ export function findDirectives( directive: string directiveLabel: string replacer?: ReplacerFn - functionId?: FunctionIdFn + generateFunctionId?: GenerateFunctionIdFn directiveSplitParam: string filename: string root: string @@ -478,8 +472,9 @@ export function findDirectives( // that we are executing const relativeFilename = path.relative(opts.root, baseFilename) let functionId = `${relativeFilename}--${functionName}` - if (opts.functionId) { - functionId = opts.functionId({ currentId: functionId }) ?? functionId + if (opts.generateFunctionId) { + functionId = + opts.generateFunctionId({ currentId: functionId }) ?? functionId // Handle cases in which the returned id conflicts with // one of the already defined ids if (functionId in directiveFnsById) { @@ -491,7 +486,6 @@ export function findDirectives( functionId = deduplicatedId } } - functionId = makeFunctionIdUrlSafe(functionId) // If a replacer is provided, replace the function with the replacer if (opts.replacer) { diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts index 51b2f01a8c3..33dee05806d 100644 --- a/packages/directive-functions-plugin/src/index.ts +++ b/packages/directive-functions-plugin/src/index.ts @@ -5,7 +5,7 @@ import { compileDirectives } from './compilers' import type { CompileDirectivesOpts, DirectiveFn, - FunctionIdFn, + GenerateFunctionIdFn, } from './compilers' import type { Plugin } from 'vite' @@ -17,7 +17,7 @@ export type { DirectiveFn, CompileDirectivesOpts, ReplacerFn, - FunctionIdFn, + GenerateFunctionIdFn, } from './compilers' export type DirectiveFunctionsViteEnvOptions = Pick< @@ -33,7 +33,7 @@ export type DirectiveFunctionsViteOptions = Pick< > & DirectiveFunctionsViteEnvOptions & { onDirectiveFnsById?: (directiveFnsById: Record) => void - functionId?: FunctionIdFn + generateFunctionId?: GenerateFunctionIdFn } const createDirectiveRx = (directive: string) => @@ -67,7 +67,7 @@ export type DirectiveFunctionsVitePluginEnvOptions = Pick< server: DirectiveFunctionsViteEnvOptions & { envName?: string } } onDirectiveFnsById?: (directiveFnsById: Record) => void - functionId?: FunctionIdFn + generateFunctionId?: GenerateFunctionIdFn } export function TanStackDirectiveFunctionsPluginEnv( @@ -138,7 +138,7 @@ function transformCode({ directive, directiveLabel, getRuntimeCode, - functionId, + generateFunctionId, replacer, onDirectiveFnsById, root, @@ -163,7 +163,7 @@ function transformCode({ directive, directiveLabel, getRuntimeCode, - functionId, + generateFunctionId, replacer, code, root, diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index e23e4b54ff1..26c2ec95574 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -6,7 +6,7 @@ import { import type { DevEnvironment, Plugin, ViteDevServer } from 'vite' import type { DirectiveFn, - FunctionIdFn, + GenerateFunctionIdFn, ReplacerFn, } from '@tanstack/directive-functions-plugin' @@ -19,7 +19,7 @@ export type ServerFnPluginOpts = { * and its modules. */ manifestVirtualImportId: string - functionId?: FunctionIdFn + generateFunctionId?: GenerateFunctionIdFn client: ServerFnPluginEnvOpts ssr: ServerFnPluginEnvOpts server: ServerFnPluginEnvOpts @@ -58,8 +58,12 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { } }, }) - const functionId = buildFunctionId((functionIdOpts, next) => - next(Boolean(viteDevServer), opts.functionId?.(functionIdOpts)), + const generateFunctionId = buildGenerateFunctionId( + (generateFunctionIdOpts, next) => + next( + Boolean(viteDevServer), + opts.generateFunctionId?.(generateFunctionIdOpts), + ), ) const directive = 'use server' @@ -74,7 +78,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { directive, directiveLabel, getRuntimeCode: opts.client.getRuntimeCode, - functionId, + generateFunctionId, replacer: opts.client.replacer, onDirectiveFnsById, }), @@ -86,7 +90,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { directive, directiveLabel, getRuntimeCode: opts.ssr.getRuntimeCode, - functionId, + generateFunctionId, replacer: opts.ssr.replacer, onDirectiveFnsById, }), @@ -136,7 +140,7 @@ export function createTanStackServerFnPlugin(opts: ServerFnPluginOpts): { directive, directiveLabel, getRuntimeCode: opts.server.getRuntimeCode, - functionId, + generateFunctionId, replacer: opts.server.replacer, onDirectiveFnsById, }), @@ -182,8 +186,12 @@ export function TanStackServerFnPluginEnv( }, }) - const functionId = buildFunctionId((functionIdOpts, next) => - next(Boolean(serverDevEnv), opts.functionId?.(functionIdOpts)), + const generateFunctionId = buildGenerateFunctionId( + (generateFunctionIdOpts, next) => + next( + Boolean(serverDevEnv), + opts.generateFunctionId?.(generateFunctionIdOpts), + ), ) const directive = 'use server' @@ -196,7 +204,7 @@ export function TanStackServerFnPluginEnv( directive, directiveLabel, onDirectiveFnsById, - functionId, + generateFunctionId, environments: { client: { envLabel: 'Client', @@ -260,12 +268,20 @@ function resolveViteId(id: string) { return `\0${id}` } -function buildFunctionId( +function makeFunctionIdUrlSafe(location: string): string { + return location + .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore + .replace(/_{2,}/g, '_') // Collapse multiple underscores + .replace(/^_|_$/g, '') // Trim leading/trailing underscores + .replace(/_--/g, '--') // Clean up the joiner +} + +function buildGenerateFunctionId( delegate: ( - opts: Parameters[0], + opts: Parameters[0], next: (dev: boolean, value?: string) => string, ) => string, -): FunctionIdFn { +): GenerateFunctionIdFn { const currentIdToGeneratedId = new Map() const generatedIds = new Set() return (opts) => { @@ -274,10 +290,10 @@ function buildFunctionId( let generatedId = currentIdToGeneratedId.get(opts.currentId) if (generatedId === undefined) { generatedId = delegate(opts, (dev, newId) => { - // If no value provided, then return the currentId on development + // If no value provided, then return the url-safe currentId on development // and SHA256 using the currentId as seed on production if (newId === undefined) { - if (dev) newId = opts.currentId + if (dev) newId = makeFunctionIdUrlSafe(opts.currentId) else newId = crypto .createHash('sha256') @@ -286,7 +302,6 @@ function buildFunctionId( } return newId }) - // Deduplicate in case the generated id conflicts with an existing id if (generatedIds.has(generatedId)) { let deduplicatedId diff --git a/packages/start-plugin-core/src/plugin.ts b/packages/start-plugin-core/src/plugin.ts index cb0a5ded33a..304389349d8 100644 --- a/packages/start-plugin-core/src/plugin.ts +++ b/packages/start-plugin-core/src/plugin.ts @@ -334,7 +334,7 @@ export function TanStackStartVitePluginCore( // This is the ID that will be available to look up and import // our server function manifest and resolve its module manifestVirtualImportId: VIRTUAL_MODULES.serverFnManifest, - functionId: startPluginOpts?.serverFns?.functionId, + generateFunctionId: startPluginOpts?.serverFns?.generateFunctionId, client: { getRuntimeCode: () => `import { createClientRpc } from '@tanstack/${corePluginOpts.framework}-start/client-rpc'`, diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index 8b94a126b39..ffea0527da6 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -159,7 +159,7 @@ const tanstackStartOptionsSchema = z serverFns: z .object({ base: z.string().optional().default('/_serverFn'), - functionId: z + generateFunctionId: z .function() .args( z.object({ From db884614d9d8ec9faa31ad92d338614cce9f0d33 Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Sun, 5 Oct 2025 22:40:59 +0300 Subject: [PATCH 07/12] Refactor `generateFunctionId` for enhanced flexibility and consistency. Update related types, remove legacy `currentId`, and streamline deduplication logic. directive-functions-plugin does not generate functionIds anymore. This is the responsibility of the server-functions-plugin --- .../server-functions/vite.config.ts | 10 ++--- .../src/compilers.ts | 29 ++++--------- .../directive-functions-plugin/src/index.ts | 4 +- packages/server-functions-plugin/src/index.ts | 43 ++++++++++--------- packages/start-plugin-core/src/schema.ts | 3 +- 5 files changed, 41 insertions(+), 48 deletions(-) diff --git a/e2e/react-start/server-functions/vite.config.ts b/e2e/react-start/server-functions/vite.config.ts index 236559775ca..97ff657630e 100644 --- a/e2e/react-start/server-functions/vite.config.ts +++ b/e2e/react-start/server-functions/vite.config.ts @@ -4,8 +4,8 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite' import viteReact from '@vitejs/plugin-react' const FUNCTIONS_WITH_CONSTANT_ID = [ - 'src/routes/submit-post-formdata.tsx--greetUser_createServerFn_handler', - 'src/routes/formdata-redirect/index.tsx--greetUser_createServerFn_handler', + 'src/routes/submit-post-formdata.tsx/greetUser_createServerFn_handler', + 'src/routes/formdata-redirect/index.tsx/greetUser_createServerFn_handler', ] export default defineConfig({ @@ -15,9 +15,9 @@ export default defineConfig({ }), tanstackStart({ serverFns: { - functionId: (opts) => { - if (FUNCTIONS_WITH_CONSTANT_ID.includes(opts.currentId)) - return 'constant_id' + generateFunctionId: (opts) => { + const id = `${opts.filename}/${opts.functionName}` + if (FUNCTIONS_WITH_CONSTANT_ID.includes(id)) return 'constant_id' else return undefined }, }, diff --git a/packages/directive-functions-plugin/src/compilers.ts b/packages/directive-functions-plugin/src/compilers.ts index a7d8fc47df0..99537674a16 100644 --- a/packages/directive-functions-plugin/src/compilers.ts +++ b/packages/directive-functions-plugin/src/compilers.ts @@ -24,8 +24,9 @@ export type SupportedFunctionPath = | babel.NodePath export type GenerateFunctionIdFn = (opts: { - currentId: string -}) => string | undefined + filename: string + functionName: string +}) => string export type ReplacerFn = (opts: { fn: string @@ -43,7 +44,7 @@ export type CompileDirectivesOpts = ParseAstOptions & { getRuntimeCode?: (opts: { directiveFnsById: Record }) => string - generateFunctionId?: GenerateFunctionIdFn + generateFunctionId: GenerateFunctionIdFn replacer: ReplacerFn // devSplitImporter: string filename: string @@ -219,7 +220,7 @@ export function findDirectives( directive: string directiveLabel: string replacer?: ReplacerFn - generateFunctionId?: GenerateFunctionIdFn + generateFunctionId: GenerateFunctionIdFn directiveSplitParam: string filename: string root: string @@ -471,22 +472,10 @@ export function findDirectives( // Relative to have constant functionId regardless of the machine // that we are executing const relativeFilename = path.relative(opts.root, baseFilename) - let functionId = `${relativeFilename}--${functionName}` - if (opts.generateFunctionId) { - functionId = - opts.generateFunctionId({ currentId: functionId }) ?? functionId - // Handle cases in which the returned id conflicts with - // one of the already defined ids - if (functionId in directiveFnsById) { - let deduplicatedId - let iteration = 0 - do { - deduplicatedId = `${functionId}_${++iteration}` - } while (deduplicatedId in directiveFnsById) - functionId = deduplicatedId - } - } - + const functionId = opts.generateFunctionId({ + filename: relativeFilename, + functionName: functionName, + }) // If a replacer is provided, replace the function with the replacer if (opts.replacer) { const replacer = opts.replacer({ diff --git a/packages/directive-functions-plugin/src/index.ts b/packages/directive-functions-plugin/src/index.ts index 33dee05806d..ff86b2f6baa 100644 --- a/packages/directive-functions-plugin/src/index.ts +++ b/packages/directive-functions-plugin/src/index.ts @@ -33,7 +33,7 @@ export type DirectiveFunctionsViteOptions = Pick< > & DirectiveFunctionsViteEnvOptions & { onDirectiveFnsById?: (directiveFnsById: Record) => void - generateFunctionId?: GenerateFunctionIdFn + generateFunctionId: GenerateFunctionIdFn } const createDirectiveRx = (directive: string) => @@ -67,7 +67,7 @@ export type DirectiveFunctionsVitePluginEnvOptions = Pick< server: DirectiveFunctionsViteEnvOptions & { envName?: string } } onDirectiveFnsById?: (directiveFnsById: Record) => void - generateFunctionId?: GenerateFunctionIdFn + generateFunctionId: GenerateFunctionIdFn } export function TanStackDirectiveFunctionsPluginEnv( diff --git a/packages/server-functions-plugin/src/index.ts b/packages/server-functions-plugin/src/index.ts index 26c2ec95574..4edee08bd03 100644 --- a/packages/server-functions-plugin/src/index.ts +++ b/packages/server-functions-plugin/src/index.ts @@ -12,6 +12,10 @@ import type { export type CreateRpcFn = (functionId: string, splitImportFn?: string) => any +export type GenerateFunctionIdFnOptional = ( + opts: Parameters[0], +) => string | undefined + export type ServerFnPluginOpts = { /** * The virtual import ID that will be used to import the server function manifest. @@ -19,7 +23,7 @@ export type ServerFnPluginOpts = { * and its modules. */ manifestVirtualImportId: string - generateFunctionId?: GenerateFunctionIdFn + generateFunctionId?: GenerateFunctionIdFnOptional client: ServerFnPluginEnvOpts ssr: ServerFnPluginEnvOpts server: ServerFnPluginEnvOpts @@ -282,39 +286,38 @@ function buildGenerateFunctionId( next: (dev: boolean, value?: string) => string, ) => string, ): GenerateFunctionIdFn { - const currentIdToGeneratedId = new Map() - const generatedIds = new Set() + const entryIdToFunctionId = new Map() + const functionIds = new Set() return (opts) => { - // Keep the previous id in case we already generated it. This is for consistency - // between client / server builds and hot reload - let generatedId = currentIdToGeneratedId.get(opts.currentId) - if (generatedId === undefined) { - generatedId = delegate(opts, (dev, newId) => { + const entryId = `${opts.filename}--${opts.functionName}` + let functionId = entryIdToFunctionId.get(entryId) + if (functionId === undefined) { + functionId = delegate(opts, (dev, updatedFunctionId) => { // If no value provided, then return the url-safe currentId on development // and SHA256 using the currentId as seed on production - if (newId === undefined) { - if (dev) newId = makeFunctionIdUrlSafe(opts.currentId) + if (updatedFunctionId === undefined) { + if (dev) updatedFunctionId = makeFunctionIdUrlSafe(entryId) else - newId = crypto + updatedFunctionId = crypto .createHash('sha256') - .update(opts.currentId) + .update(entryId) .digest('hex') } - return newId + return updatedFunctionId }) // Deduplicate in case the generated id conflicts with an existing id - if (generatedIds.has(generatedId)) { + if (functionIds.has(functionId)) { let deduplicatedId let iteration = 0 do { - deduplicatedId = `${generatedId}_${++iteration}` - } while (generatedIds.has(deduplicatedId)) - generatedId = deduplicatedId + deduplicatedId = `${functionId}_${++iteration}` + } while (functionIds.has(deduplicatedId)) + functionId = deduplicatedId } - currentIdToGeneratedId.set(opts.currentId, generatedId) - generatedIds.add(generatedId) + entryIdToFunctionId.set(entryId, functionId) + functionIds.add(functionId) } - return generatedId + return functionId } } diff --git a/packages/start-plugin-core/src/schema.ts b/packages/start-plugin-core/src/schema.ts index ffea0527da6..39d471447da 100644 --- a/packages/start-plugin-core/src/schema.ts +++ b/packages/start-plugin-core/src/schema.ts @@ -163,7 +163,8 @@ const tanstackStartOptionsSchema = z .function() .args( z.object({ - currentId: z.string(), + filename: z.string(), + functionName: z.string(), }), ) .returns(z.string().optional()) From e1ee92c1573004b7281ff2fc41d570118233a0fa Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Sun, 5 Oct 2025 23:03:17 +0300 Subject: [PATCH 08/12] Fixed failing tests --- .../tests/compiler.test.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts index a943f7f0803..33061d151f3 100644 --- a/packages/directive-functions-plugin/tests/compiler.test.ts +++ b/packages/directive-functions-plugin/tests/compiler.test.ts @@ -3,12 +3,25 @@ import { describe, expect, test } from 'vitest' import { compileDirectives } from '../src/compilers' import type { CompileDirectivesOpts } from '../src/compilers' +function makeFunctionIdUrlSafe(location: string): string { + return location + .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore + .replace(/_{2,}/g, '_') // Collapse multiple underscores + .replace(/^_|_$/g, '') // Trim leading/trailing underscores + .replace(/_--/g, '--') // Clean up the joiner +} + +const generateFunctionId: CompileDirectivesOpts['generateFunctionId'] = (opts) => { + return makeFunctionIdUrlSafe(`${opts.filename}--${opts.functionName}`) +} + const clientConfig: Omit = { directive: 'use server', directiveLabel: 'Server function', root: './test-files', - filename: 'test.ts', + filename: './test-files/test.ts', getRuntimeCode: () => 'import { createClientRpc } from "my-rpc-lib-client"', + generateFunctionId, replacer: (opts) => `createClientRpc(${JSON.stringify(opts.functionId)})`, } @@ -16,8 +29,9 @@ const ssrConfig: Omit = { directive: 'use server', directiveLabel: 'Server function', root: './test-files', - filename: 'test.ts', + filename: './test-files/test.ts', getRuntimeCode: () => 'import { createSsrRpc } from "my-rpc-lib-server"', + generateFunctionId, replacer: (opts) => `createSsrRpc(${JSON.stringify(opts.functionId)})`, } @@ -25,8 +39,9 @@ const serverConfig: Omit = { directive: 'use server', directiveLabel: 'Server function', root: './test-files', - filename: 'test.ts', + filename: './test-files/test.ts', getRuntimeCode: () => 'import { createServerRpc } from "my-rpc-lib-server"', + generateFunctionId, replacer: (opts) => // On the server build, we need different code for the split function // vs any other server functions the split function may reference From 713975ccd91c96a1e478d88773a4ce4be775e545 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:04:52 +0000 Subject: [PATCH 09/12] ci: apply automated fixes --- packages/directive-functions-plugin/tests/compiler.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/directive-functions-plugin/tests/compiler.test.ts b/packages/directive-functions-plugin/tests/compiler.test.ts index 33061d151f3..6381e124267 100644 --- a/packages/directive-functions-plugin/tests/compiler.test.ts +++ b/packages/directive-functions-plugin/tests/compiler.test.ts @@ -11,7 +11,9 @@ function makeFunctionIdUrlSafe(location: string): string { .replace(/_--/g, '--') // Clean up the joiner } -const generateFunctionId: CompileDirectivesOpts['generateFunctionId'] = (opts) => { +const generateFunctionId: CompileDirectivesOpts['generateFunctionId'] = ( + opts, +) => { return makeFunctionIdUrlSafe(`${opts.filename}--${opts.functionName}`) } From ec08c2f9b388b658d2370ca8b6ef41d8f4f296c7 Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Mon, 6 Oct 2025 00:22:13 +0300 Subject: [PATCH 10/12] Document function ID generation process and customization options for server functions. --- .../start/framework/react/server-functions.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/start/framework/react/server-functions.md b/docs/start/framework/react/server-functions.md index 6e0a0e7386d..6a6b2868237 100644 --- a/docs/start/framework/react/server-functions.md +++ b/docs/start/framework/react/server-functions.md @@ -224,6 +224,50 @@ 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 + +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). + +Customization: + +You can customize the ID generation by passing a `generateFunctionId` option to the server-functions plugin during bundler setup. If your function returns `undefined`, the default strategy above is used. + +Example: + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' + +export default defineConfig({ + plugins: [ + tanstackStart({ + 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 undefined + }, + }, + }), + react(), + ], +}) +``` + +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. From 3a67fc9952edae52a984a4c54707ab39de17c911 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 21:25:02 +0000 Subject: [PATCH 11/12] ci: apply automated fixes --- docs/start/framework/react/server-functions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/start/framework/react/server-functions.md b/docs/start/framework/react/server-functions.md index 6a6b2868237..d35d60c8431 100644 --- a/docs/start/framework/react/server-functions.md +++ b/docs/start/framework/react/server-functions.md @@ -229,6 +229,7 @@ Handle request cancellation with `AbortSignal` for long-running operations. 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. @@ -264,6 +265,7 @@ 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**. From ad7bd9d385d65b3a565b5f888a1a5d75042fd81c Mon Sep 17 00:00:00 2001 From: Dimitris Zenios Date: Mon, 6 Oct 2025 00:31:11 +0300 Subject: [PATCH 12/12] Minor documentation fix --- docs/start/framework/react/server-functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/start/framework/react/server-functions.md b/docs/start/framework/react/server-functions.md index d35d60c8431..82c649636da 100644 --- a/docs/start/framework/react/server-functions.md +++ b/docs/start/framework/react/server-functions.md @@ -237,7 +237,7 @@ Defaults: Customization: -You can customize the ID generation by passing a `generateFunctionId` option to the server-functions plugin during bundler setup. If your function returns `undefined`, the default strategy above is used. +You can customize function ID generation by providing a `generateFunctionId` function when configuring the TanStack Start Vite plugin: Example: