+
+ Middleware factories with server imports are stripped from client
+ build
+
+
)
diff --git a/e2e/react-start/server-functions/src/routes/middleware/middleware-factory.tsx b/e2e/react-start/server-functions/src/routes/middleware/middleware-factory.tsx
new file mode 100644
index 00000000000..2c4979ad370
--- /dev/null
+++ b/e2e/react-start/server-functions/src/routes/middleware/middleware-factory.tsx
@@ -0,0 +1,126 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { createMiddleware, createServerFn } from '@tanstack/react-start'
+import { getRequestHeaders } from '@tanstack/react-start/server'
+import React from 'react'
+
+/**
+ * This test verifies that middleware factories (functions that return createMiddleware().server())
+ * have their server-only code properly stripped from the client bundle.
+ *
+ * If the .server() part inside the factory is not stripped from the client build, this will fail with:
+ * "Module node:async_hooks has been externalized for browser compatibility"
+ * because @tanstack/react-start/server uses node:async_hooks internally.
+ */
+
+// Middleware factory function - returns a middleware with .server() call
+function createHeaderMiddleware(headerName: string) {
+ return createMiddleware({ type: 'function' }).server(async ({ next }) => {
+ // Use a server-only import - this should be stripped from client build
+ const headers = getRequestHeaders()
+ const headerValue = headers.get(headerName) ?? 'missing'
+
+ console.log(`[middleware-factory] ${headerName}:`, headerValue)
+
+ return next({
+ context: {
+ headerName,
+ headerValue,
+ },
+ })
+ })
+}
+
+// Arrow function factory variant
+const createPrefixedHeaderMiddleware = (prefix: string) => {
+ return createMiddleware({ type: 'function' }).server(async ({ next }) => {
+ // Use a server-only import - this should be stripped from client build
+ const headers = getRequestHeaders()
+ const allHeaderNames = [...headers.keys()]
+ const prefixedHeaders = allHeaderNames.filter((name) =>
+ name.toLowerCase().startsWith(prefix.toLowerCase()),
+ )
+
+ console.log(
+ `[middleware-factory] Prefixed headers (${prefix}):`,
+ prefixedHeaders,
+ )
+
+ return next({
+ context: {
+ prefix,
+ matchedHeaders: prefixedHeaders,
+ },
+ })
+ })
+}
+
+// Create middleware instances using the factories
+const customHeaderMiddleware = createHeaderMiddleware('x-custom-factory-header')
+const prefixedMiddleware = createPrefixedHeaderMiddleware('x-factory-')
+
+const serverFn = createServerFn()
+ .middleware([customHeaderMiddleware, prefixedMiddleware])
+ .handler(async ({ context }) => {
+ return {
+ headerName: context.headerName,
+ headerValue: context.headerValue,
+ prefix: context.prefix,
+ matchedHeaders: context.matchedHeaders,
+ }
+ })
+
+export const Route = createFileRoute('/middleware/middleware-factory')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const [result, setResult] = React.useState<{
+ headerName: string
+ headerValue: string
+ prefix: string
+ matchedHeaders: Array
+ } | null>(null)
+ const [error, setError] = React.useState(null)
+
+ async function handleClick() {
+ try {
+ const data = await serverFn({
+ headers: {
+ 'x-custom-factory-header': 'factory-header-value',
+ 'x-factory-one': 'one',
+ 'x-factory-two': 'two',
+ },
+ })
+ setResult(data)
+ setError(null)
+ } catch (e) {
+ setResult(null)
+ setError(e instanceof Error ? e.message : String(e))
+ }
+ }
+
+ return (
+
+
Middleware Factory Test
+
+ This test verifies that middleware factories (functions returning
+ createMiddleware().server()) have their server-only code properly
+ stripped from the client build.
+
+
+ {result && (
+
+
{result.headerValue}
+
+ {result.matchedHeaders.join(',')}
+
+
+ )}
+ {error && (
+
Error: {error}
+ )}
+
+ )
+}
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 5848f73a612..a428f5640ea 100644
--- a/e2e/react-start/server-functions/tests/server-functions.spec.ts
+++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts
@@ -635,3 +635,32 @@ test('server-only imports in middleware.server() are stripped from client build'
page.getByTestId('server-import-middleware-result'),
).toContainText('test-header-value')
})
+
+test('middleware factories with server-only imports are stripped from client build', async ({
+ page,
+}) => {
+ // This test verifies that middleware factories (functions returning createMiddleware().server())
+ // with server-only imports are properly stripped from the client build.
+ // If the .server() part inside the factory is not removed, the build would fail with
+ // node:async_hooks externalization errors because getRequestHeaders uses node:async_hooks internally.
+ // The fact that this page loads at all proves the server code was stripped correctly.
+ await page.goto('/middleware/middleware-factory')
+
+ await page.waitForLoadState('networkidle')
+
+ // Click the button to call the server function with factory middlewares
+ await page.getByTestId('test-middleware-factory-btn').click()
+
+ // Wait for the result - should contain our custom header value from the factory middleware
+ await expect(page.getByTestId('header-value')).toContainText(
+ 'factory-header-value',
+ )
+
+ // Also verify the prefixed headers were matched correctly
+ await expect(page.getByTestId('matched-headers')).toContainText(
+ 'x-factory-one',
+ )
+ await expect(page.getByTestId('matched-headers')).toContainText(
+ 'x-factory-two',
+ )
+})
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
index 9ad4c4e5aa8..ef5b7457de6 100644
--- a/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/compiler.ts
@@ -68,6 +68,57 @@ const LookupSetup: Record = {
ClientOnlyFn: { type: 'directCall' },
}
+// Single source of truth for detecting which kinds are present in code
+// These patterns are used for:
+// 1. Pre-scanning code to determine which kinds to look for (before AST parsing)
+// 2. Deriving the plugin's transform code filter
+export const KindDetectionPatterns: Record = {
+ ServerFn: /\.handler\s*\(/,
+ Middleware: /createMiddleware/,
+ IsomorphicFn: /createIsomorphicFn/,
+ ServerOnlyFn: /createServerOnlyFn/,
+ ClientOnlyFn: /createClientOnlyFn/,
+}
+
+// Which kinds are valid for each environment
+export const LookupKindsPerEnv: Record<'client' | 'server', Set> = {
+ client: new Set([
+ 'Middleware',
+ 'ServerFn',
+ 'IsomorphicFn',
+ 'ServerOnlyFn',
+ 'ClientOnlyFn',
+ ] as const),
+ server: new Set([
+ 'ServerFn',
+ 'IsomorphicFn',
+ 'ServerOnlyFn',
+ 'ClientOnlyFn',
+ ] as const),
+}
+
+/**
+ * Detects which LookupKinds are present in the code using string matching.
+ * This is a fast pre-scan before AST parsing to limit the work done during compilation.
+ */
+export function detectKindsInCode(
+ code: string,
+ env: 'client' | 'server',
+): Set {
+ const detected = new Set()
+ const validForEnv = LookupKindsPerEnv[env]
+
+ for (const [kind, pattern] of Object.entries(KindDetectionPatterns) as Array<
+ [LookupKind, RegExp]
+ >) {
+ if (validForEnv.has(kind) && pattern.test(code)) {
+ detected.add(kind)
+ }
+ }
+
+ return detected
+}
+
// Pre-computed map: identifier name -> Set for fast candidate detection (method chain only)
// Multiple kinds can share the same identifier (e.g., 'server' and 'client' are used by both Middleware and IsomorphicFn)
const IdentifierToKinds = new Map>()
@@ -86,6 +137,16 @@ for (const [kind, setup] of Object.entries(LookupSetup) as Array<
}
}
+// Known factory function names for direct call and root-as-candidate patterns
+// These are the names that, when called directly, create a new function.
+// Used to filter nested candidates - we only want to include actual factory calls,
+// not invocations of already-created functions (e.g., `myServerFn()` should NOT be a candidate)
+const DirectCallFactoryNames = new Set([
+ 'createServerOnlyFn',
+ 'createClientOnlyFn',
+ 'createIsomorphicFn',
+])
+
export type LookupConfig = {
libName: string
rootExport: string
@@ -100,13 +161,79 @@ interface ModuleInfo {
reExportAllSources: Array
}
+/**
+ * Computes whether any file kinds need direct-call candidate detection.
+ * This includes both directCall types (ServerOnlyFn, ClientOnlyFn) and
+ * allowRootAsCandidate types (IsomorphicFn).
+ */
+function needsDirectCallDetection(kinds: Set): boolean {
+ for (const kind of kinds) {
+ const setup = LookupSetup[kind]
+ if (setup.type === 'directCall' || setup.allowRootAsCandidate) {
+ return true
+ }
+ }
+ return false
+}
+
+/**
+ * Checks if a CallExpression is a direct-call candidate for NESTED detection.
+ * Returns true if the callee is a known factory function name.
+ * This is stricter than top-level detection because we need to filter out
+ * invocations of existing server functions (e.g., `myServerFn()`).
+ */
+function isNestedDirectCallCandidate(node: t.CallExpression): boolean {
+ let calleeName: string | undefined
+ if (t.isIdentifier(node.callee)) {
+ calleeName = node.callee.name
+ } else if (
+ t.isMemberExpression(node.callee) &&
+ t.isIdentifier(node.callee.property)
+ ) {
+ calleeName = node.callee.property.name
+ }
+ return calleeName !== undefined && DirectCallFactoryNames.has(calleeName)
+}
+
+/**
+ * Checks if a CallExpression path is a top-level direct-call candidate.
+ * Top-level means the call is the init of a VariableDeclarator at program level.
+ * We accept any simple identifier call or namespace call at top level
+ * (e.g., `isomorphicFn()`, `TanStackStart.createServerOnlyFn()`) and let
+ * resolution verify it. This handles renamed imports.
+ */
+function isTopLevelDirectCallCandidate(
+ path: babel.NodePath,
+): boolean {
+ const node = path.node
+
+ // Must be a simple identifier call or namespace call
+ const isSimpleCall =
+ t.isIdentifier(node.callee) ||
+ (t.isMemberExpression(node.callee) &&
+ t.isIdentifier(node.callee.object) &&
+ t.isIdentifier(node.callee.property))
+
+ if (!isSimpleCall) {
+ return false
+ }
+
+ // Must be top-level: VariableDeclarator -> VariableDeclaration -> Program
+ const parent = path.parent
+ if (!t.isVariableDeclarator(parent) || parent.init !== node) {
+ return false
+ }
+ const grandParent = path.parentPath.parent
+ if (!t.isVariableDeclaration(grandParent)) {
+ return false
+ }
+ return t.isProgram(path.parentPath.parentPath?.parent)
+}
+
export class ServerFnCompiler {
private moduleCache = new Map()
private initialized = false
private validLookupKinds: Set
- // Precomputed flags for candidate detection (avoid recomputing on each collectCandidates call)
- private hasDirectCallKinds: boolean
- private hasRootAsCandidateKinds: boolean
// Fast lookup for direct imports from known libraries (e.g., '@tanstack/react-start')
// Maps: libName → (exportName → Kind)
// This allows O(1) resolution for the common case without async resolveId calls
@@ -122,18 +249,6 @@ export class ServerFnCompiler {
},
) {
this.validLookupKinds = options.lookupKinds
-
- // Precompute flags for candidate detection
- this.hasDirectCallKinds = false
- this.hasRootAsCandidateKinds = false
- for (const kind of options.lookupKinds) {
- const setup = LookupSetup[kind]
- if (setup.type === 'directCall') {
- this.hasDirectCallKinds = true
- } else if (setup.allowRootAsCandidate) {
- this.hasRootAsCandidateKinds = true
- }
- }
}
private async init() {
@@ -310,68 +425,106 @@ export class ServerFnCompiler {
code,
id,
isProviderFile,
+ detectedKinds,
}: {
code: string
id: string
isProviderFile: boolean
+ /** Pre-detected kinds present in this file. If not provided, all valid kinds are checked. */
+ detectedKinds?: Set
}) {
if (!this.initialized) {
await this.init()
}
- const { info, ast } = this.ingestModule({ code, id })
- const candidates = this.collectCandidates(info.bindings)
- if (candidates.length === 0) {
- // this hook will only be invoked if there is `.handler(` | `.server(` | `.client(` in the code,
- // so not discovering a handler candidate is rather unlikely, but maybe possible?
+
+ // Use detected kinds if provided, otherwise fall back to all valid kinds for this env
+ const fileKinds = detectedKinds
+ ? new Set([...detectedKinds].filter((k) => this.validLookupKinds.has(k)))
+ : this.validLookupKinds
+
+ // Early exit if no kinds to process
+ if (fileKinds.size === 0) {
return null
}
- // let's find out which of the candidates are actually server functions
- // Resolve all candidates in parallel for better performance
+ const checkDirectCalls = needsDirectCallDetection(fileKinds)
+
+ const { ast } = this.ingestModule({ code, id })
+
+ // Single-pass traversal to:
+ // 1. Collect candidate paths (only candidates, not all CallExpressions)
+ // 2. Build a map for looking up paths of nested calls in method chains
+ const candidatePaths: Array> = []
+ // Map for nested chain lookup - only populated for CallExpressions that are
+ // part of a method chain (callee.object is a CallExpression)
+ const chainCallPaths = new Map<
+ t.CallExpression,
+ babel.NodePath
+ >()
+
+ babel.traverse(ast, {
+ CallExpression: (path) => {
+ const node = path.node
+ const parent = path.parent
+
+ // Check if this call is part of a larger chain (inner call)
+ // If so, store it for method chain lookup but don't treat as candidate
+ if (
+ t.isMemberExpression(parent) &&
+ t.isCallExpression(path.parentPath.parent)
+ ) {
+ // This is an inner call in a chain - store for later lookup
+ chainCallPaths.set(node, path)
+ return
+ }
+
+ // Pattern 1: Method chain pattern (.handler(), .server(), .client(), etc.)
+ if (isMethodChainCandidate(node, fileKinds)) {
+ candidatePaths.push(path)
+ return
+ }
+
+ // Pattern 2: Direct call pattern
+ if (checkDirectCalls) {
+ if (isTopLevelDirectCallCandidate(path)) {
+ candidatePaths.push(path)
+ } else if (isNestedDirectCallCandidate(node)) {
+ candidatePaths.push(path)
+ }
+ }
+ },
+ })
+
+ if (candidatePaths.length === 0) {
+ return null
+ }
+
+ // Resolve all candidates in parallel to determine their kinds
const resolvedCandidates = await Promise.all(
- candidates.map(async (candidate) => ({
- candidate,
- kind: await this.resolveExprKind(candidate, id),
+ candidatePaths.map(async (path) => ({
+ path,
+ kind: await this.resolveExprKind(path.node, id),
})),
)
- // Map from candidate/root node -> kind
- // Note: For top-level variable declarations, candidate === root (the outermost CallExpression)
- const toRewriteMap = new Map()
- for (const { candidate, kind } of resolvedCandidates) {
- if (this.validLookupKinds.has(kind as LookupKind)) {
- toRewriteMap.set(candidate, kind as LookupKind)
- }
- }
- if (toRewriteMap.size === 0) {
+ // Filter to valid candidates
+ const validCandidates = resolvedCandidates.filter(({ kind }) =>
+ this.validLookupKinds.has(kind as LookupKind),
+ ) as Array<{ path: babel.NodePath; kind: LookupKind }>
+
+ if (validCandidates.length === 0) {
return null
}
- // Single-pass traversal to find NodePaths and collect method chains
+ // Process valid candidates to collect method chains
const pathsToRewrite: Array<{
path: babel.NodePath
kind: LookupKind
methodChain: MethodChainPaths
}> = []
- // First, collect all CallExpression paths in the AST for O(1) lookup
- const callExprPaths = new Map<
- t.CallExpression,
- babel.NodePath
- >()
-
- babel.traverse(ast, {
- CallExpression(path) {
- callExprPaths.set(path.node, path)
- },
- })
-
- // Now process candidates - we can look up any CallExpression path in O(1)
- for (const [node, kind] of toRewriteMap) {
- const path = callExprPaths.get(node)
- if (!path) {
- continue
- }
+ for (const { path, kind } of validCandidates) {
+ const node = path.node
// Collect method chain paths by walking DOWN from root through the chain
const methodChain: MethodChainPaths = {
@@ -384,6 +537,8 @@ export class ServerFnCompiler {
// Walk down the call chain using nodes, look up paths from map
let currentNode: t.CallExpression = node
+ let currentPath: babel.NodePath = path
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
const callee = currentNode.callee
@@ -395,7 +550,6 @@ export class ServerFnCompiler {
if (t.isIdentifier(callee.property)) {
const name = callee.property.name as keyof MethodChainPaths
if (name in methodChain) {
- const currentPath = callExprPaths.get(currentNode)!
// Get first argument path
const args = currentPath.get('arguments')
const firstArgPath =
@@ -412,18 +566,17 @@ export class ServerFnCompiler {
break
}
currentNode = callee.object
+ // Look up path from chain map, or use candidate path if not found
+ const nextPath = chainCallPaths.get(currentNode)
+ if (!nextPath) {
+ break
+ }
+ currentPath = nextPath
}
pathsToRewrite.push({ path, kind, methodChain })
}
- // Verify we found all candidates (pathsToRewrite should have same size as toRewriteMap had)
- if (pathsToRewrite.length !== toRewriteMap.size) {
- throw new Error(
- `Internal error: could not find all paths to rewrite. please file an issue`,
- )
- }
-
const refIdents = findReferencedIdentifiers(ast)
for (const { path, kind, methodChain } of pathsToRewrite) {
@@ -461,42 +614,6 @@ export class ServerFnCompiler {
})
}
- // collects all candidate CallExpressions at top-level
- private collectCandidates(bindings: Map) {
- const candidates: Array = []
-
- for (const binding of bindings.values()) {
- if (binding.type === 'var' && t.isCallExpression(binding.init)) {
- // Pattern 1: Method chain pattern (.handler(), .server(), etc.)
- const methodChainCandidate = isCandidateCallExpression(
- binding.init,
- this.validLookupKinds,
- )
- if (methodChainCandidate) {
- candidates.push(methodChainCandidate)
- continue
- }
-
- // Pattern 2: Direct call pattern
- // Handles:
- // - createServerOnlyFn(), createClientOnlyFn() (direct call kinds)
- // - createIsomorphicFn() (root-as-candidate kinds)
- // - TanStackStart.createServerOnlyFn() (namespace calls)
- if (this.hasDirectCallKinds || this.hasRootAsCandidateKinds) {
- if (
- t.isIdentifier(binding.init.callee) ||
- (t.isMemberExpression(binding.init.callee) &&
- t.isIdentifier(binding.init.callee.property))
- ) {
- // Include as candidate - kind resolution will verify it's actually a known export
- candidates.push(binding.init)
- }
- }
- }
- }
- return candidates
- }
-
private async resolveIdentifierKind(
ident: string,
id: string,
@@ -793,15 +910,17 @@ export class ServerFnCompiler {
}
}
-function isCandidateCallExpression(
- node: t.Node | null | undefined,
+/**
+ * Checks if a CallExpression has a method chain pattern that matches any of the lookup kinds.
+ * E.g., `.handler()`, `.server()`, `.client()`, `.createMiddlewares()`
+ */
+function isMethodChainCandidate(
+ node: t.CallExpression,
lookupKinds: Set,
-): t.CallExpression | undefined {
- if (!t.isCallExpression(node)) return undefined
-
+): boolean {
const callee = node.callee
if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.property)) {
- return undefined
+ return false
}
// Use pre-computed map for O(1) lookup
@@ -811,10 +930,10 @@ function isCandidateCallExpression(
// Check if any of the possible kinds are in the valid lookup kinds
for (const kind of possibleKinds) {
if (lookupKinds.has(kind)) {
- return node
+ return true
}
}
}
- return undefined
+ return false
}
diff --git a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
index f19ffbda102..ab6a618c741 100644
--- a/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts
@@ -1,28 +1,35 @@
import { TRANSFORM_ID_REGEX } from '../constants'
-import { ServerFnCompiler } from './compiler'
+import {
+ KindDetectionPatterns,
+ LookupKindsPerEnv,
+ ServerFnCompiler,
+ detectKindsInCode,
+} from './compiler'
import type { CompileStartFrameworkOptions } from '../types'
import type { LookupConfig, LookupKind } from './compiler'
import type { PluginOption } from 'vite'
function cleanId(id: string): string {
+ // Remove null byte prefix used by Vite/Rollup for virtual modules
+ if (id.startsWith('\0')) {
+ id = id.slice(1)
+ }
const queryIndex = id.indexOf('?')
return queryIndex === -1 ? id : id.substring(0, queryIndex)
}
-const LookupKindsPerEnv: Record<'client' | 'server', Set> = {
- client: new Set([
- 'Middleware',
- 'ServerFn',
- 'IsomorphicFn',
- 'ServerOnlyFn',
- 'ClientOnlyFn',
- ] as const),
- server: new Set([
- 'ServerFn',
- 'IsomorphicFn',
- 'ServerOnlyFn',
- 'ClientOnlyFn',
- ] as const),
+// Derive transform code filter from KindDetectionPatterns (single source of truth)
+function getTransformCodeFilterForEnv(env: 'client' | 'server'): Array {
+ const validKinds = LookupKindsPerEnv[env]
+ const patterns: Array = []
+ for (const [kind, pattern] of Object.entries(KindDetectionPatterns) as Array<
+ [LookupKind, RegExp]
+ >) {
+ if (validKinds.has(kind)) {
+ patterns.push(pattern)
+ }
+ }
+ return patterns
}
const getLookupConfigurationsForEnv = (
@@ -77,12 +84,6 @@ function buildDirectiveSplitParam(directive: string) {
return `tsr-directive-${directive.replace(/[^a-zA-Z0-9]/g, '-')}`
}
-const commonTransformCodeFilter = [
- /\.\s*handler\(/,
- /createIsomorphicFn/,
- /createServerOnlyFn/,
- /createClientOnlyFn/,
-]
export function createServerFnPlugin(opts: {
framework: CompileStartFrameworkOptions
directive: string
@@ -95,16 +96,8 @@ export function createServerFnPlugin(opts: {
name: string
type: 'client' | 'server'
}): PluginOption {
- // Code filter patterns for transform functions:
- // - `.handler(` for createServerFn
- // - `createMiddleware(` for middleware (client only)
- // - `createIsomorphicFn` for isomorphic functions
- // - `createServerOnlyFn` for server-only functions
- // - `createClientOnlyFn` for client-only functions
- const transformCodeFilter =
- environment.type === 'client'
- ? [...commonTransformCodeFilter, /createMiddleware\s*\(/]
- : commonTransformCodeFilter
+ // Derive transform code filter from KindDetectionPatterns (single source of truth)
+ const transformCodeFilter = getTransformCodeFilterForEnv(environment.type)
return {
name: `tanstack-start-core::server-fn:${environment.name}`,
@@ -172,8 +165,16 @@ export function createServerFnPlugin(opts: {
const isProviderFile = id.includes(directiveSplitParam)
+ // Detect which kinds are present in this file before parsing
+ const detectedKinds = detectKindsInCode(code, environment.type)
+
id = cleanId(id)
- const result = await compiler.compile({ id, code, isProviderFile })
+ const result = await compiler.compile({
+ id,
+ code,
+ isProviderFile,
+ detectedKinds,
+ })
return result
},
},
diff --git a/packages/start-plugin-core/tests/compiler.test.ts b/packages/start-plugin-core/tests/compiler.test.ts
new file mode 100644
index 00000000000..14aac107532
--- /dev/null
+++ b/packages/start-plugin-core/tests/compiler.test.ts
@@ -0,0 +1,393 @@
+import { describe, expect, test } from 'vitest'
+import {
+ detectKindsInCode,
+ ServerFnCompiler,
+} from '../src/create-server-fn-plugin/compiler'
+import type {
+ LookupConfig,
+ LookupKind,
+} from '../src/create-server-fn-plugin/compiler'
+
+// Helper to create a compiler with all kinds enabled
+function createFullCompiler(env: 'client' | 'server') {
+ const lookupKinds: Set =
+ env === 'client'
+ ? new Set([
+ 'ServerFn',
+ 'Middleware',
+ 'IsomorphicFn',
+ 'ServerOnlyFn',
+ 'ClientOnlyFn',
+ ])
+ : new Set(['ServerFn', 'IsomorphicFn', 'ServerOnlyFn', 'ClientOnlyFn'])
+
+ const lookupConfigurations: Array = [
+ {
+ libName: '@tanstack/react-start',
+ rootExport: 'createServerFn',
+ kind: 'Root',
+ },
+ {
+ libName: '@tanstack/react-start',
+ rootExport: 'createMiddleware',
+ kind: 'Root',
+ },
+ {
+ libName: '@tanstack/react-start',
+ rootExport: 'createIsomorphicFn',
+ kind: 'IsomorphicFn',
+ },
+ {
+ libName: '@tanstack/react-start',
+ rootExport: 'createServerOnlyFn',
+ kind: 'ServerOnlyFn',
+ },
+ {
+ libName: '@tanstack/react-start',
+ rootExport: 'createClientOnlyFn',
+ kind: 'ClientOnlyFn',
+ },
+ ]
+
+ return new ServerFnCompiler({
+ env,
+ directive: 'use server',
+ lookupKinds,
+ lookupConfigurations,
+ loadModule: async () => {},
+ resolveId: async (id) => id,
+ })
+}
+
+describe('detectKindsInCode', () => {
+ describe('detects individual kinds correctly', () => {
+ test('detects ServerFn via .handler(', () => {
+ const code = `
+ import { createServerFn } from '@tanstack/react-start'
+ const fn = createServerFn().handler(() => 'hello')
+ `
+ expect(detectKindsInCode(code, 'client')).toEqual(new Set(['ServerFn']))
+ expect(detectKindsInCode(code, 'server')).toEqual(new Set(['ServerFn']))
+ })
+
+ test('detects Middleware', () => {
+ const code = `
+ import { createMiddleware } from '@tanstack/react-start'
+ const mw = createMiddleware().server(({ next }) => next())
+ `
+ // Middleware is only valid on client
+ expect(detectKindsInCode(code, 'client')).toEqual(new Set(['Middleware']))
+ expect(detectKindsInCode(code, 'server')).toEqual(new Set())
+ })
+
+ test('detects IsomorphicFn', () => {
+ const code = `
+ import { createIsomorphicFn } from '@tanstack/react-start'
+ const fn = createIsomorphicFn().client(() => 'c').server(() => 's')
+ `
+ expect(detectKindsInCode(code, 'client')).toEqual(
+ new Set(['IsomorphicFn']),
+ )
+ expect(detectKindsInCode(code, 'server')).toEqual(
+ new Set(['IsomorphicFn']),
+ )
+ })
+
+ test('detects ServerOnlyFn', () => {
+ const code = `
+ import { createServerOnlyFn } from '@tanstack/react-start'
+ const fn = createServerOnlyFn(() => 'server only')
+ `
+ expect(detectKindsInCode(code, 'client')).toEqual(
+ new Set(['ServerOnlyFn']),
+ )
+ expect(detectKindsInCode(code, 'server')).toEqual(
+ new Set(['ServerOnlyFn']),
+ )
+ })
+
+ test('detects ClientOnlyFn', () => {
+ const code = `
+ import { createClientOnlyFn } from '@tanstack/react-start'
+ const fn = createClientOnlyFn(() => 'client only')
+ `
+ expect(detectKindsInCode(code, 'client')).toEqual(
+ new Set(['ClientOnlyFn']),
+ )
+ expect(detectKindsInCode(code, 'server')).toEqual(
+ new Set(['ClientOnlyFn']),
+ )
+ })
+ })
+
+ describe('detects multiple kinds in same file', () => {
+ test('detects ServerFn and IsomorphicFn', () => {
+ const code = `
+ import { createServerFn, createIsomorphicFn } from '@tanstack/react-start'
+ const serverFn = createServerFn().handler(() => 'hello')
+ const isoFn = createIsomorphicFn().client(() => 'client')
+ `
+ expect(detectKindsInCode(code, 'client')).toEqual(
+ new Set(['ServerFn', 'IsomorphicFn']),
+ )
+ })
+
+ test('detects all kinds on client', () => {
+ const code = `
+ import { createServerFn, createMiddleware, createIsomorphicFn, createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start'
+ const a = createServerFn().handler(() => {})
+ const b = createMiddleware().server(({ next }) => next())
+ const c = createIsomorphicFn()
+ const d = createServerOnlyFn(() => {})
+ const e = createClientOnlyFn(() => {})
+ `
+ expect(detectKindsInCode(code, 'client')).toEqual(
+ new Set([
+ 'ServerFn',
+ 'Middleware',
+ 'IsomorphicFn',
+ 'ServerOnlyFn',
+ 'ClientOnlyFn',
+ ]),
+ )
+ })
+
+ test('detects all valid kinds on server (excludes Middleware)', () => {
+ const code = `
+ import { createServerFn, createMiddleware, createIsomorphicFn, createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start'
+ const a = createServerFn().handler(() => {})
+ const b = createMiddleware().server(({ next }) => next())
+ const c = createIsomorphicFn()
+ const d = createServerOnlyFn(() => {})
+ const e = createClientOnlyFn(() => {})
+ `
+ // Middleware should NOT be detected on server
+ expect(detectKindsInCode(code, 'server')).toEqual(
+ new Set(['ServerFn', 'IsomorphicFn', 'ServerOnlyFn', 'ClientOnlyFn']),
+ )
+ })
+ })
+
+ describe('handles edge cases', () => {
+ test('returns empty set for code with no matching patterns', () => {
+ const code = `
+ const foo = 'bar'
+ function hello() { return 'world' }
+ `
+ expect(detectKindsInCode(code, 'client')).toEqual(new Set())
+ expect(detectKindsInCode(code, 'server')).toEqual(new Set())
+ })
+
+ test('handles .handler with whitespace variations', () => {
+ const code1 = `fn.handler(() => {})`
+ const code2 = `fn.handler (() => {})`
+ const code3 = `fn.handler\n(() => {})`
+ const code4 = `fn.handler\t(() => {})`
+
+ expect(detectKindsInCode(code1, 'client')).toEqual(new Set(['ServerFn']))
+ expect(detectKindsInCode(code2, 'client')).toEqual(new Set(['ServerFn']))
+ expect(detectKindsInCode(code3, 'client')).toEqual(new Set(['ServerFn']))
+ expect(detectKindsInCode(code4, 'client')).toEqual(new Set(['ServerFn']))
+ })
+
+ test('does not false positive on similar names', () => {
+ const code = `
+ const myCreateServerFn = () => {}
+ const createServerFnLike = () => {}
+ // But this should match:
+ createServerFn().handler(() => {})
+ `
+ // Should only match because of .handler(, not because of variable names
+ expect(detectKindsInCode(code, 'client')).toEqual(new Set(['ServerFn']))
+ })
+ })
+})
+
+describe('compiler handles multiple files with different kinds', () => {
+ test('single compiler instance correctly processes files with different kinds in succession', async () => {
+ const compiler = createFullCompiler('client')
+
+ // File 1: ServerFn only
+ const result1 = await compiler.compile({
+ code: `
+ import { createServerFn } from '@tanstack/react-start'
+ export const fn = createServerFn().handler(() => 'hello')
+ `,
+ id: 'file1.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['ServerFn']),
+ })
+ expect(result1).not.toBeNull()
+ expect(result1!.code).toContain('__executeServer') // Client should have RPC stub
+ expect(result1!.code).not.toContain('createMiddleware')
+ expect(result1!.code).not.toContain('createIsomorphicFn')
+
+ // File 2: Middleware only
+ const result2 = await compiler.compile({
+ code: `
+ import { createMiddleware } from '@tanstack/react-start'
+ export const mw = createMiddleware().server(({ next }) => {
+ console.log('server only')
+ return next()
+ })
+ `,
+ id: 'file2.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['Middleware']),
+ })
+ expect(result2).not.toBeNull()
+ // .server() should be stripped on client
+ expect(result2!.code).not.toContain('console.log')
+ expect(result2!.code).not.toContain('createServerFn')
+
+ // File 3: IsomorphicFn only
+ const result3 = await compiler.compile({
+ code: `
+ import { createIsomorphicFn } from '@tanstack/react-start'
+ export const fn = createIsomorphicFn()
+ .client(() => 'client-value')
+ .server(() => 'server-value')
+ `,
+ id: 'file3.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['IsomorphicFn']),
+ })
+ expect(result3).not.toBeNull()
+ // Client should have client implementation
+ expect(result3!.code).toContain('client-value')
+ expect(result3!.code).not.toContain('server-value')
+
+ // File 4: ServerOnlyFn only
+ const result4 = await compiler.compile({
+ code: `
+ import { createServerOnlyFn } from '@tanstack/react-start'
+ export const fn = createServerOnlyFn(() => 'server only value')
+ `,
+ id: 'file4.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['ServerOnlyFn']),
+ })
+ expect(result4).not.toBeNull()
+ // Client should have error throw
+ expect(result4!.code).toContain('throw new Error')
+ expect(result4!.code).not.toContain('server only value')
+
+ // File 5: Mix of ServerFn and IsomorphicFn
+ const result5 = await compiler.compile({
+ code: `
+ import { createServerFn, createIsomorphicFn } from '@tanstack/react-start'
+ export const serverFn = createServerFn().handler(() => 'hello')
+ export const isoFn = createIsomorphicFn().client(() => 'client-iso')
+ `,
+ id: 'file5.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['ServerFn', 'IsomorphicFn']),
+ })
+ expect(result5).not.toBeNull()
+ expect(result5!.code).toContain('__executeServer') // ServerFn RPC
+ expect(result5!.code).toContain('client-iso') // IsomorphicFn client impl
+ })
+
+ test('compiler works correctly when processing same kind multiple times', async () => {
+ const compiler = createFullCompiler('client')
+
+ // First file with IsomorphicFn
+ const result1 = await compiler.compile({
+ code: `
+ import { createIsomorphicFn } from '@tanstack/react-start'
+ export const fn1 = createIsomorphicFn().client(() => 'first')
+ `,
+ id: 'first.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['IsomorphicFn']),
+ })
+ expect(result1!.code).toContain('first')
+
+ // Second file with different IsomorphicFn
+ const result2 = await compiler.compile({
+ code: `
+ import { createIsomorphicFn } from '@tanstack/react-start'
+ export const fn2 = createIsomorphicFn().client(() => 'second')
+ `,
+ id: 'second.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['IsomorphicFn']),
+ })
+ expect(result2!.code).toContain('second')
+ expect(result2!.code).not.toContain('first') // Should not leak from previous file
+ })
+
+ test('server environment excludes Middleware from detected kinds', async () => {
+ const compiler = createFullCompiler('server')
+
+ // Even if Middleware is in detectedKinds, server env should ignore it
+ const result = await compiler.compile({
+ code: `
+ import { createMiddleware } from '@tanstack/react-start'
+ export const mw = createMiddleware().server(({ next }) => next())
+ `,
+ id: 'middleware.ts',
+ isProviderFile: false,
+ // Intentionally including Middleware even though it's server env
+ detectedKinds: new Set(['Middleware']),
+ })
+ // Should return null since Middleware is not valid on server
+ expect(result).toBeNull()
+ })
+})
+
+describe('edge cases for detectedKinds', () => {
+ test('empty detectedKinds returns null (no candidates to process)', async () => {
+ const compiler = createFullCompiler('client')
+
+ const result = await compiler.compile({
+ code: `
+ // This file somehow passed Vite filter but has no actual kinds
+ const foo = 'bar'
+ // Maybe a false positive match like "handler(" in a comment
+ // .handler( in string
+ `,
+ id: 'empty.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(), // Empty set
+ })
+
+ expect(result).toBeNull()
+ })
+
+ test('detectedKinds not provided falls back to all valid kinds', async () => {
+ const compiler = createFullCompiler('client')
+
+ // When detectedKinds is not provided, should check all valid kinds
+ const result = await compiler.compile({
+ code: `
+ import { createIsomorphicFn } from '@tanstack/react-start'
+ export const fn = createIsomorphicFn().client(() => 'works')
+ `,
+ id: 'no-detected.ts',
+ isProviderFile: false,
+ // No detectedKinds provided
+ })
+
+ expect(result).not.toBeNull()
+ expect(result!.code).toContain('works')
+ })
+
+ test('detectedKinds with invalid kinds for env are filtered out', async () => {
+ const compiler = createFullCompiler('server')
+
+ // Passing Middleware (invalid for server) along with valid kind
+ const result = await compiler.compile({
+ code: `
+ import { createIsomorphicFn } from '@tanstack/react-start'
+ export const fn = createIsomorphicFn().server(() => 'server-impl')
+ `,
+ id: 'filtered.ts',
+ isProviderFile: false,
+ detectedKinds: new Set(['Middleware', 'IsomorphicFn']), // Middleware should be filtered
+ })
+
+ expect(result).not.toBeNull()
+ expect(result!.code).toContain('server-impl')
+ })
+})
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnFactory.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnFactory.tsx
new file mode 100644
index 00000000000..803bb6d8011
--- /dev/null
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnFactory.tsx
@@ -0,0 +1,25 @@
+// Isomorphic function factory - returns a createIsomorphicFn with .client() and .server() calls
+export function createPlatformFn(platform: string) {
+ return () => `client-${platform}`;
+}
+
+// Arrow function factory with only server implementation
+export const createServerImplFn = (name: string) => {
+ return () => {};
+};
+
+// Arrow function factory with only client implementation
+export const createClientImplFn = (name: string) => {
+ return () => {
+ console.log(`Client: ${name}`);
+ return `client-${name}`;
+ };
+};
+
+// Factory returning no-implementation isomorphic fn
+export function createNoImplFn() {
+ return () => {};
+}
+
+// Top-level isomorphic fn for comparison
+export const topLevelIsomorphicFn = () => 'client';
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnInline.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnInline.tsx
new file mode 100644
index 00000000000..09666623a62
--- /dev/null
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/client/createIsomorphicFnInline.tsx
@@ -0,0 +1,41 @@
+// Create and immediately invoke isomorphic fn inside a function
+export function getPlatformValue() {
+ const fn = () => 'client-value';
+ return fn();
+}
+
+// Arrow function that creates and invokes isomorphic fn
+export const getEnvironment = () => {
+ const envFn = () => 'running on client';
+ return envFn();
+};
+
+// Create isomorphic fn inline without assigning to variable
+export function getDirectValue() {
+ return (() => 'direct-client')();
+}
+
+// Multiple isomorphic fns created and used in same function
+export function getMultipleValues() {
+ const first = () => 'first-client';
+ const second = () => 'second-client';
+ return {
+ first: first(),
+ second: second()
+ };
+}
+
+// Isomorphic fn with server-only implementation used inline
+export function getServerOnlyValue() {
+ const fn = () => {};
+ return fn();
+}
+
+// Isomorphic fn with client-only implementation used inline
+export function getClientOnlyValue() {
+ const fn = () => {
+ console.log('client side effect');
+ return 'client-only-value';
+ };
+ return fn();
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnFactory.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnFactory.tsx
new file mode 100644
index 00000000000..ee949e0c382
--- /dev/null
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnFactory.tsx
@@ -0,0 +1,25 @@
+// Isomorphic function factory - returns a createIsomorphicFn with .client() and .server() calls
+export function createPlatformFn(platform: string) {
+ return () => `server-${platform}`;
+}
+
+// Arrow function factory with only server implementation
+export const createServerImplFn = (name: string) => {
+ return () => {
+ console.log(`Server: ${name}`);
+ return `server-${name}`;
+ };
+};
+
+// Arrow function factory with only client implementation
+export const createClientImplFn = (name: string) => {
+ return () => {};
+};
+
+// Factory returning no-implementation isomorphic fn
+export function createNoImplFn() {
+ return () => {};
+}
+
+// Top-level isomorphic fn for comparison
+export const topLevelIsomorphicFn = () => 'server';
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnInline.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnInline.tsx
new file mode 100644
index 00000000000..9c058dfae86
--- /dev/null
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/snapshots/server/createIsomorphicFnInline.tsx
@@ -0,0 +1,41 @@
+// Create and immediately invoke isomorphic fn inside a function
+export function getPlatformValue() {
+ const fn = () => 'server-value';
+ return fn();
+}
+
+// Arrow function that creates and invokes isomorphic fn
+export const getEnvironment = () => {
+ const envFn = () => 'running on server';
+ return envFn();
+};
+
+// Create isomorphic fn inline without assigning to variable
+export function getDirectValue() {
+ return (() => 'direct-server')();
+}
+
+// Multiple isomorphic fns created and used in same function
+export function getMultipleValues() {
+ const first = () => 'first-server';
+ const second = () => 'second-server';
+ return {
+ first: first(),
+ second: second()
+ };
+}
+
+// Isomorphic fn with server-only implementation used inline
+export function getServerOnlyValue() {
+ const fn = () => {
+ console.log('server side effect');
+ return 'server-only-value';
+ };
+ return fn();
+}
+
+// Isomorphic fn with client-only implementation used inline
+export function getClientOnlyValue() {
+ const fn = () => {};
+ return fn();
+}
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnFactory.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnFactory.tsx
new file mode 100644
index 00000000000..3974ce77f08
--- /dev/null
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnFactory.tsx
@@ -0,0 +1,34 @@
+import { createIsomorphicFn } from '@tanstack/react-start'
+
+// Isomorphic function factory - returns a createIsomorphicFn with .client() and .server() calls
+export function createPlatformFn(platform: string) {
+ return createIsomorphicFn()
+ .client(() => `client-${platform}`)
+ .server(() => `server-${platform}`)
+}
+
+// Arrow function factory with only server implementation
+export const createServerImplFn = (name: string) => {
+ return createIsomorphicFn().server(() => {
+ console.log(`Server: ${name}`)
+ return `server-${name}`
+ })
+}
+
+// Arrow function factory with only client implementation
+export const createClientImplFn = (name: string) => {
+ return createIsomorphicFn().client(() => {
+ console.log(`Client: ${name}`)
+ return `client-${name}`
+ })
+}
+
+// Factory returning no-implementation isomorphic fn
+export function createNoImplFn() {
+ return createIsomorphicFn()
+}
+
+// Top-level isomorphic fn for comparison
+export const topLevelIsomorphicFn = createIsomorphicFn()
+ .client(() => 'client')
+ .server(() => 'server')
diff --git a/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnInline.tsx b/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnInline.tsx
new file mode 100644
index 00000000000..1b4145bf9c5
--- /dev/null
+++ b/packages/start-plugin-core/tests/createIsomorphicFn/test-files/createIsomorphicFnInline.tsx
@@ -0,0 +1,55 @@
+import { createIsomorphicFn } from '@tanstack/react-start'
+
+// Create and immediately invoke isomorphic fn inside a function
+export function getPlatformValue() {
+ const fn = createIsomorphicFn()
+ .client(() => 'client-value')
+ .server(() => 'server-value')
+ return fn()
+}
+
+// Arrow function that creates and invokes isomorphic fn
+export const getEnvironment = () => {
+ const envFn = createIsomorphicFn()
+ .server(() => 'running on server')
+ .client(() => 'running on client')
+ return envFn()
+}
+
+// Create isomorphic fn inline without assigning to variable
+export function getDirectValue() {
+ return createIsomorphicFn()
+ .client(() => 'direct-client')
+ .server(() => 'direct-server')()
+}
+
+// Multiple isomorphic fns created and used in same function
+export function getMultipleValues() {
+ const first = createIsomorphicFn()
+ .client(() => 'first-client')
+ .server(() => 'first-server')
+
+ const second = createIsomorphicFn()
+ .client(() => 'second-client')
+ .server(() => 'second-server')
+
+ return { first: first(), second: second() }
+}
+
+// Isomorphic fn with server-only implementation used inline
+export function getServerOnlyValue() {
+ const fn = createIsomorphicFn().server(() => {
+ console.log('server side effect')
+ return 'server-only-value'
+ })
+ return fn()
+}
+
+// Isomorphic fn with client-only implementation used inline
+export function getClientOnlyValue() {
+ const fn = createIsomorphicFn().client(() => {
+ console.log('client side effect')
+ return 'client-only-value'
+ })
+ return fn()
+}
diff --git a/packages/start-plugin-core/tests/createMiddleware/snapshots/client/middleware-factory.ts b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/middleware-factory.ts
new file mode 100644
index 00000000000..6845bd0ef5d
--- /dev/null
+++ b/packages/start-plugin-core/tests/createMiddleware/snapshots/client/middleware-factory.ts
@@ -0,0 +1,20 @@
+import { createMiddleware } from '@tanstack/react-start';
+
+// Middleware factory function - returns a middleware with .server() call
+export function createPublicRateLimitMiddleware(keySuffix) {
+ return createMiddleware({
+ type: 'function'
+ });
+}
+
+// Arrow function factory
+export const createAuthMiddleware = requiredRole => {
+ return createMiddleware({
+ type: 'function'
+ });
+};
+
+// Top-level middleware for comparison
+export const topLevelMiddleware = createMiddleware({
+ id: 'topLevel'
+});
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/createMiddleware/test-files/middleware-factory.ts b/packages/start-plugin-core/tests/createMiddleware/test-files/middleware-factory.ts
new file mode 100644
index 00000000000..a306dbf3712
--- /dev/null
+++ b/packages/start-plugin-core/tests/createMiddleware/test-files/middleware-factory.ts
@@ -0,0 +1,41 @@
+import { createMiddleware } from '@tanstack/react-start'
+
+// Middleware factory function - returns a middleware with .server() call
+export function createPublicRateLimitMiddleware(keySuffix) {
+ return createMiddleware({ type: 'function' }).server(
+ async ({ next, data }) => {
+ const key = keySuffix ? `ip:${keySuffix}` : 'ip:default'
+ const finalKey = typeof data === 'string' ? `ip:${data}` : key
+
+ const { success } = await rateLimit({ key: finalKey })
+
+ if (!success) {
+ throw new Error('Too many requests')
+ }
+
+ return next()
+ },
+ )
+}
+
+// Arrow function factory
+export const createAuthMiddleware = (requiredRole) => {
+ return createMiddleware({ type: 'function' }).server(async ({ next }) => {
+ const user = await getUser()
+ if (!user) {
+ throw new Error('Unauthorized')
+ }
+ if (requiredRole && user.role !== requiredRole) {
+ throw new Error('Forbidden')
+ }
+ return next()
+ })
+}
+
+// Top-level middleware for comparison
+export const topLevelMiddleware = createMiddleware({
+ id: 'topLevel',
+}).server(async ({ next }) => {
+ console.log('top level')
+ return next()
+})
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyFactory.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyFactory.tsx
new file mode 100644
index 00000000000..6a29fcbfe64
--- /dev/null
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/client/envOnlyFactory.tsx
@@ -0,0 +1,32 @@
+// Server-only function factory
+export function createServerFactory(name: string) {
+ return () => {
+ throw new Error("createServerOnlyFn() functions can only be called on the server!");
+ };
+}
+
+// Client-only function factory
+export function createClientFactory(name: string) {
+ return () => {
+ console.log(`Client only: ${name}`);
+ return `client-${name}`;
+ };
+}
+
+// Arrow function server factory
+export const createServerArrowFactory = (prefix: string) => {
+ return () => {
+ throw new Error("createServerOnlyFn() functions can only be called on the server!");
+ };
+};
+
+// Arrow function client factory
+export const createClientArrowFactory = (prefix: string) => {
+ return () => `${prefix}-client`;
+};
+
+// Top-level for comparison
+export const topLevelServerFn = () => {
+ throw new Error("createServerOnlyFn() functions can only be called on the server!");
+};
+export const topLevelClientFn = () => 'client';
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyFactory.tsx b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyFactory.tsx
new file mode 100644
index 00000000000..58b2483a48d
--- /dev/null
+++ b/packages/start-plugin-core/tests/envOnly/snapshots/server/envOnlyFactory.tsx
@@ -0,0 +1,32 @@
+// Server-only function factory
+export function createServerFactory(name: string) {
+ return () => {
+ console.log(`Server only: ${name}`);
+ return `server-${name}`;
+ };
+}
+
+// Client-only function factory
+export function createClientFactory(name: string) {
+ return () => {
+ throw new Error("createClientOnlyFn() functions can only be called on the client!");
+ };
+}
+
+// Arrow function server factory
+export const createServerArrowFactory = (prefix: string) => {
+ return () => `${prefix}-server`;
+};
+
+// Arrow function client factory
+export const createClientArrowFactory = (prefix: string) => {
+ return () => {
+ throw new Error("createClientOnlyFn() functions can only be called on the client!");
+ };
+};
+
+// Top-level for comparison
+export const topLevelServerFn = () => 'server';
+export const topLevelClientFn = () => {
+ throw new Error("createClientOnlyFn() functions can only be called on the client!");
+};
\ No newline at end of file
diff --git a/packages/start-plugin-core/tests/envOnly/test-files/envOnlyFactory.tsx b/packages/start-plugin-core/tests/envOnly/test-files/envOnlyFactory.tsx
new file mode 100644
index 00000000000..a1223a6aeec
--- /dev/null
+++ b/packages/start-plugin-core/tests/envOnly/test-files/envOnlyFactory.tsx
@@ -0,0 +1,31 @@
+import { createServerOnlyFn, createClientOnlyFn } from '@tanstack/react-start'
+
+// Server-only function factory
+export function createServerFactory(name: string) {
+ return createServerOnlyFn(() => {
+ console.log(`Server only: ${name}`)
+ return `server-${name}`
+ })
+}
+
+// Client-only function factory
+export function createClientFactory(name: string) {
+ return createClientOnlyFn(() => {
+ console.log(`Client only: ${name}`)
+ return `client-${name}`
+ })
+}
+
+// Arrow function server factory
+export const createServerArrowFactory = (prefix: string) => {
+ return createServerOnlyFn(() => `${prefix}-server`)
+}
+
+// Arrow function client factory
+export const createClientArrowFactory = (prefix: string) => {
+ return createClientOnlyFn(() => `${prefix}-client`)
+}
+
+// Top-level for comparison
+export const topLevelServerFn = createServerOnlyFn(() => 'server')
+export const topLevelClientFn = createClientOnlyFn(() => 'client')