From f3eece968dfd349a4726e8cd9b1c3dfd083ee67e Mon Sep 17 00:00:00 2001 From: kylekz Date: Fri, 26 Dec 2025 00:58:38 +1300 Subject: [PATCH 1/4] fix: detect .handler() when separated by whitespace --- .../src/start-compiler-plugin/compiler.ts | 2 +- .../createServerFn/createServerFn.test.ts | 69 ++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts index ee441008d6c..dc97d3a5651 100644 --- a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts +++ b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts @@ -87,7 +87,7 @@ const LookupSetup: Record< // 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*\(/, + ServerFn: /\.\s*handler\s*\(/, Middleware: /createMiddleware/, IsomorphicFn: /createIsomorphicFn/, ServerOnlyFn: /createServerOnlyFn/, diff --git a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts index a039321781f..05c724ba20f 100644 --- a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts +++ b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts @@ -1,7 +1,10 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { describe, expect, test, vi } from 'vitest' -import { StartCompiler } from '../../src/start-compiler-plugin/compiler' +import { + StartCompiler, + detectKindsInCode, +} from '../../src/start-compiler-plugin/compiler' // Default test options for StartCompiler function getDefaultTestOptions(env: 'client' | 'server') { @@ -302,4 +305,68 @@ describe('createServerFn compiles correctly', async () => { ) expect(resolveIdMock).toHaveBeenNthCalledWith(2, './factory', 'test.ts') }) + + test('should detect .handler() when dot and handler are separated by whitespace/newlines (regression test for server code leak)', async () => { + // This test reproduces a bug where server code leaked into client bundles. + // When Vite/Babel reformats code during the transform pipeline, the dot can + // end up on the previous line: + // .middleware([authMiddleware]). + // handler(async () => {...}) + // The old regex /\.handler\s*\(/ didn't match this pattern because it required + // dot and handler to be contiguous. The fix changes it to /\.\s*handler\s*\(/ + const reformattedCode = ` + import { createServerFn, createMiddleware } from '@tanstack/react-start' + const authMiddleware = createMiddleware({ type: 'function' }).server( + async ({ next }) => { + return next({ context: { user: 1 } }) + } + ) + const $getRandomSong = createServerFn({ + method: 'POST' + }). + middleware([authMiddleware]). + handler(async () => { + // Server-only code that should NOT leak to client + return 'song' + })` + + // This is the key part: in the real plugin flow, detectKindsInCode is called first + // to determine which kinds are present in the code. If the regex doesn't match, + // the ServerFn kind won't be detected and the handler won't be transformed. + const detectedKinds = detectKindsInCode(reformattedCode, 'client') + + // The detection should find ServerFn even with the reformatted code + expect(detectedKinds.has('ServerFn')).toBe(true) + + // Now compile with the detected kinds (simulating real plugin flow) + const compiler = new StartCompiler({ + env: 'client', + ...getDefaultTestOptions('client'), + loadModule: async () => {}, + lookupKinds: new Set(['ServerFn']), + lookupConfigurations: [ + { + libName: '@tanstack/react-start', + rootExport: 'createServerFn', + kind: 'Root', + }, + ], + resolveId: async (id) => id, + }) + + const compiledResultClient = await compiler.compile({ + code: reformattedCode, + id: 'test.ts', + detectedKinds, + }) + + // The key assertion: client code should NOT contain the handler implementation + // Instead, it should have createClientRpc() which is the RPC stub + expect(compiledResultClient).not.toBeNull() + expect(compiledResultClient!.code).toContain('createClientRpc') + + // Should NOT contain the actual handler implementation + expect(compiledResultClient!.code).not.toContain('Server-only code') + expect(compiledResultClient!.code).not.toContain("return 'song'") + }) }) From c4589d088f0c18348ab68c2d43ff2b28437b5055 Mon Sep 17 00:00:00 2001 From: kylekz Date: Fri, 26 Dec 2025 01:46:25 +1300 Subject: [PATCH 2/4] fix: also match createServerFn( --- .../src/start-compiler-plugin/compiler.ts | 2 +- .../start-plugin-core/tests/compiler.test.ts | 31 ++++++++- .../createServerFn/createServerFn.test.ts | 69 +------------------ 3 files changed, 32 insertions(+), 70 deletions(-) diff --git a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts index dc97d3a5651..059db7bcc36 100644 --- a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts +++ b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts @@ -87,7 +87,7 @@ const LookupSetup: Record< // 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: /\.\s*handler\s*\(/, + ServerFn: /createServerFn\s*\(|\.\s*handler\s*\(/, Middleware: /createMiddleware/, IsomorphicFn: /createIsomorphicFn/, ServerOnlyFn: /createServerOnlyFn/, diff --git a/packages/start-plugin-core/tests/compiler.test.ts b/packages/start-plugin-core/tests/compiler.test.ts index de92d56693c..b92c3774216 100644 --- a/packages/start-plugin-core/tests/compiler.test.ts +++ b/packages/start-plugin-core/tests/compiler.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest' import { - detectKindsInCode, StartCompiler, + detectKindsInCode, } from '../src/start-compiler-plugin/compiler' import type { LookupConfig, @@ -201,6 +201,35 @@ describe('detectKindsInCode', () => { expect(detectKindsInCode(code4, 'client')).toEqual(new Set(['ServerFn'])) }) + test('handles whitespace between . and handler (reformatted code)', () => { + // When Vite/Babel reformats code, the dot can end up on a previous line + const code1 = `fn.\nhandler(() => {})` + const code2 = `fn.\n handler(() => {})` + const code3 = `fn. \n handler(() => {})` + + expect(detectKindsInCode(code1, 'client')).toEqual(new Set(['ServerFn'])) + expect(detectKindsInCode(code2, 'client')).toEqual(new Set(['ServerFn'])) + expect(detectKindsInCode(code3, 'client')).toEqual(new Set(['ServerFn'])) + }) + + test('detects createServerFn() call directly', () => { + // The pattern should match createServerFn() calls, not just .handler() + const code = ` + import { createServerFn } from '@tanstack/react-start' + const fn = createServerFn() + ` + expect(detectKindsInCode(code, 'client')).toEqual(new Set(['ServerFn'])) + }) + + test('does not false positive on similar function names', () => { + // createServerFnExample() should NOT match - only exact createServerFn( should + const code = ` + const fn = createServerFnExample() + const fn2 = createServerFnLike() + ` + expect(detectKindsInCode(code, 'client')).toEqual(new Set()) + }) + test('does not false positive on similar names', () => { const code = ` const myCreateServerFn = () => {} diff --git a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts index 05c724ba20f..a039321781f 100644 --- a/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts +++ b/packages/start-plugin-core/tests/createServerFn/createServerFn.test.ts @@ -1,10 +1,7 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { describe, expect, test, vi } from 'vitest' -import { - StartCompiler, - detectKindsInCode, -} from '../../src/start-compiler-plugin/compiler' +import { StartCompiler } from '../../src/start-compiler-plugin/compiler' // Default test options for StartCompiler function getDefaultTestOptions(env: 'client' | 'server') { @@ -305,68 +302,4 @@ describe('createServerFn compiles correctly', async () => { ) expect(resolveIdMock).toHaveBeenNthCalledWith(2, './factory', 'test.ts') }) - - test('should detect .handler() when dot and handler are separated by whitespace/newlines (regression test for server code leak)', async () => { - // This test reproduces a bug where server code leaked into client bundles. - // When Vite/Babel reformats code during the transform pipeline, the dot can - // end up on the previous line: - // .middleware([authMiddleware]). - // handler(async () => {...}) - // The old regex /\.handler\s*\(/ didn't match this pattern because it required - // dot and handler to be contiguous. The fix changes it to /\.\s*handler\s*\(/ - const reformattedCode = ` - import { createServerFn, createMiddleware } from '@tanstack/react-start' - const authMiddleware = createMiddleware({ type: 'function' }).server( - async ({ next }) => { - return next({ context: { user: 1 } }) - } - ) - const $getRandomSong = createServerFn({ - method: 'POST' - }). - middleware([authMiddleware]). - handler(async () => { - // Server-only code that should NOT leak to client - return 'song' - })` - - // This is the key part: in the real plugin flow, detectKindsInCode is called first - // to determine which kinds are present in the code. If the regex doesn't match, - // the ServerFn kind won't be detected and the handler won't be transformed. - const detectedKinds = detectKindsInCode(reformattedCode, 'client') - - // The detection should find ServerFn even with the reformatted code - expect(detectedKinds.has('ServerFn')).toBe(true) - - // Now compile with the detected kinds (simulating real plugin flow) - const compiler = new StartCompiler({ - env: 'client', - ...getDefaultTestOptions('client'), - loadModule: async () => {}, - lookupKinds: new Set(['ServerFn']), - lookupConfigurations: [ - { - libName: '@tanstack/react-start', - rootExport: 'createServerFn', - kind: 'Root', - }, - ], - resolveId: async (id) => id, - }) - - const compiledResultClient = await compiler.compile({ - code: reformattedCode, - id: 'test.ts', - detectedKinds, - }) - - // The key assertion: client code should NOT contain the handler implementation - // Instead, it should have createClientRpc() which is the RPC stub - expect(compiledResultClient).not.toBeNull() - expect(compiledResultClient!.code).toContain('createClientRpc') - - // Should NOT contain the actual handler implementation - expect(compiledResultClient!.code).not.toContain('Server-only code') - expect(compiledResultClient!.code).not.toContain("return 'song'") - }) }) From d3240ddd90f24afd4b9a83dfdc1635a8f4265a03 Mon Sep 17 00:00:00 2001 From: kylekz Date: Fri, 26 Dec 2025 01:50:53 +1300 Subject: [PATCH 3/4] fix: use word boundary for createServerFn regex --- .../start-plugin-core/src/start-compiler-plugin/compiler.ts | 2 +- packages/start-plugin-core/tests/compiler.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts index 059db7bcc36..17968211e6a 100644 --- a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts +++ b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts @@ -87,7 +87,7 @@ const LookupSetup: Record< // 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: /createServerFn\s*\(|\.\s*handler\s*\(/, + ServerFn: /\bcreateServerFn\s*\(|\.\s*handler\s*\(/, Middleware: /createMiddleware/, IsomorphicFn: /createIsomorphicFn/, ServerOnlyFn: /createServerOnlyFn/, diff --git a/packages/start-plugin-core/tests/compiler.test.ts b/packages/start-plugin-core/tests/compiler.test.ts index b92c3774216..2678fc87cfd 100644 --- a/packages/start-plugin-core/tests/compiler.test.ts +++ b/packages/start-plugin-core/tests/compiler.test.ts @@ -222,10 +222,12 @@ describe('detectKindsInCode', () => { }) test('does not false positive on similar function names', () => { - // createServerFnExample() should NOT match - only exact createServerFn( should + // Only exact createServerFn( should match, not variations const code = ` const fn = createServerFnExample() const fn2 = createServerFnLike() + const fn3 = mycreateServerFn() + const fn4 = _createServerFn() ` expect(detectKindsInCode(code, 'client')).toEqual(new Set()) }) From 016fc611fcca5197f5b5fb7945d5d3a392fa55ea Mon Sep 17 00:00:00 2001 From: Manuel Schiller Date: Thu, 25 Dec 2025 14:29:44 +0100 Subject: [PATCH 4/4] make regex more permissive --- .../start-plugin-core/src/start-compiler-plugin/compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts index 17968211e6a..70ae1be911b 100644 --- a/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts +++ b/packages/start-plugin-core/src/start-compiler-plugin/compiler.ts @@ -87,7 +87,7 @@ const LookupSetup: Record< // 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: /\bcreateServerFn\s*\(|\.\s*handler\s*\(/, + ServerFn: /\bcreateServerFn\b|\.\s*handler\s*\(/, Middleware: /createMiddleware/, IsomorphicFn: /createIsomorphicFn/, ServerOnlyFn: /createServerOnlyFn/,