Server-fn transform filter regex rejects whitespace between `.` and `handler`/`server`/`client`, silently disabling transform after AST-roundtripping pre-plugins
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Summary
@tanstack/start-plugin-core's server-fn Vite plugin uses a transform filter that requires .handler(, .server(, or .client( to appear with no whitespace between the dot and the method name. Any pre-plugin that AST-roundtrips the file (parse → generate via Babel) will reformat long createServerFn().inputValidator().handler() chains across multiple lines — putting a newline between . and the method name — and the file then silently fails the filter. The server-fn transform is skipped, the .server.ts file ships raw to the client, and every server-only import it touches (db, drizzle-orm/node-postgres, etc.) leaks into the browser bundle.
Concretely, in our project the file looked like this in source:
export const ping = createServerFn({ method: 'GET' })
.inputValidator((d: { x?: string } = {}) => d)
.handler(async () => 'ok')
After @tanstack/devtools-vite's enhanceConsoleLog plugin (which only intends to rewrite console.log/error calls but parse→generate's the whole file via @babel/parser + @babel/generator), the same code arrives at start-plugin-core's transform filter as:
export const ping = createServerFn({ method: 'GET' }).
inputValidator((d: { x?: string; } = {}) => d).
handler(async () => 'ok');
— i.e. .\nhandler(, .\ninputValidator(. The filter regex /\.handler\(/ (in packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts) does not match across the newline, so the transform handler is never invoked. The compiler's compile() is never called. The file is served as-is, post-devtools-enhance, with import { db } from '@/db/index' (etc.) intact in the client bundle. Symptom is ReferenceError: Buffer is not defined from pg-protocol's top-level Buffer.from(...), app-wide, on first page load.
Environment
@tanstack/start-plugin-core: 1.132.0@tanstack/react-start: 1.132.0@tanstack/devtools-vite: 0.3.12 (other AST-roundtripping plugin would trigger the same)vite: 7.3.1- Node 22, Bun 1.x, macOS 24.6.0
Verified mechanism
I added a node:fs.appendFileSync trace in both create-server-fn-plugin/plugin.js (transform handler entry) and create-server-fn-plugin/compiler.js (compile() entry). For the broken file, the transform handler is never called; the filter rejects the file before it gets there. For files where the filter regex still finds at least one adjacent .handler( somewhere in the code — even if other call sites in the same file are split across newlines — the filter accepts and compile() runs normally.
In our project, saved-views.server.ts happens to keep one createServerFn({ method: 'GET' }).handler(\n adjacent (single-method short chain that fits on a line after Babel reformats), so its filter check passes and the rest of the file gets transformed. documents.server.ts and the minimum repro below have every method preceded by a newline after Babel reformats, so the filter rejects them entirely.
Minimum repro
vite.config.ts:
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [
devtools(), // enhancedLogs defaults to enabled — any AST-roundtripping plugin would do
tanstackStart(),
],
})
src/example.server.ts:
import { createServerFn } from '@tanstack/react-start'
function helper() { console.log('x') } // any console.log/error to trigger devtools' AST roundtrip
export const ping = createServerFn({ method: 'GET' })
.inputValidator((d: { x?: string } = {}) => d)
.handler(async () => 'ok')
Start dev server. curl http://localhost:3000/src/example.server.ts:
- Expected: server-fn transformed —
.handler(...)replaced withcreateClientRpc(...)stub; helper body removed by dead-code elimination. - Actual: file served raw —
helper, theconsole.logcall, and the fullcreateServerFn(...).inputValidator(...).handler(async () => 'ok')chain all preserved verbatim. NocreateClientRpcimport.
Trigger requirements (verified by bisect on a fresh dev server, rm -rf node_modules/.vite/deps && bun run dev, single curl request to the test file):
Has console.log/console.error in any code outside handler bodies? |
Has .inputValidator(...) in the chain? |
server-fn transform |
|---|---|---|
| no | no | works |
| no | yes | works |
| yes | no | works |
| yes | yes | broken |
The console.log/error requirement is because devtools-vite's enhanceConsoleLog only AST-roundtrips files that match console\. and contain log / error (per enhance-logs.ts). The inputValidator requirement is because Babel's generator only splits the chain across lines when it's long enough — a bare createServerFn().handler() stays single-line and keeps .handler( adjacent.
Fix
One-line change to packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts:
code: {
- include: [/\.handler\(/, /\.server\(/, /\.client\(/]
+ include: [/\.\s*handler\(/, /\.\s*server\(/, /\.\s*client\(/]
}
The compiler's actual AST walker already handles whitespace between . and the method name correctly — the bug is purely in the regex pre-filter that decides whether to run the compiler at all. I patched my local dist/esm/create-server-fn-plugin/plugin.js with this change and re-ran with enhancedLogs: { enabled: true }: all three of .server.ts files in our app transform cleanly (.handler(...) → createClientRpc(...) stubs, server-only imports stripped, no Buffer errors).
Workaround
If you can't patch start-plugin-core, disable the AST-roundtripping pre-plugin. For us that's:
devtools({
enhancedLogs: { enabled: false },
})
Cost: lose the source-URL annotation on console.log/console.error output in dev. JSX data-tsd-source injection (a separate plugin in devtools-vite) is unaffected.
Why this is worth fixing in start-plugin-core (not just devtools-vite)
The filter is brittle by design — any current or future Vite plugin that runs enforce: 'pre' and roundtrips the AST will trigger the same bug. devtools-vite's enhanceConsoleLog is just the most likely encounter today, but anyone writing a Babel plugin that touches .server.ts files (codemod, instrumentation, source-mapper, etc.) will hit it. The fix is one regex change with no behavior risk: the AST walker is the source of truth; the regex is just a fast-path filter, and being slightly more permissive in the filter just sends a few more files into the AST walker, which already handles or rejects them correctly.
Failure mode is severe
Symptom is app-wide ReferenceError: Buffer is not defined, every page blank on load, with no parse error and no warnings logged anywhere. The natural debugging path leads to either the .server.ts file content or the server-fn compiler — neither is wrong. I lost a few hours to it; an earlier debugging pass on this same bug landed on a wrong root cause and a fix that wouldn't have helped.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/start-plugin-core/src/create-server-fn-plugin/plugin.ts and inspect the transform filter's include regexes, then compare them with the compiler entry point in the same plugin. Reproduce the AST-roundtripped chain from the issue and verify that whitespace-separated .handler(), .server(), and .client() calls still reach compile() and produce the transformed client output.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, vite
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100