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 with createClientRpc(...) stub; helper body removed by dead-code elimination.
- Actual: file served raw —
helper, the console.log call, and the full createServerFn(...).inputValidator(...).handler(async () => 'ok') chain all preserved verbatim. No createClientRpc import.
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.
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 longcreateServerFn().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.tsfile 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:
After
@tanstack/devtools-vite'senhanceConsoleLogplugin (which only intends to rewriteconsole.log/errorcalls but parse→generate's the whole file via@babel/parser+@babel/generator), the same code arrives atstart-plugin-core's transform filter as:— i.e.
.\nhandler(,.\ninputValidator(. The filter regex/\.handler\(/(inpackages/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'scompile()is never called. The file is served as-is, post-devtools-enhance, withimport { db } from '@/db/index'(etc.) intact in the client bundle. Symptom isReferenceError: Buffer is not definedfrompg-protocol's top-levelBuffer.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.1Verified mechanism
I added a
node:fs.appendFileSynctrace in bothcreate-server-fn-plugin/plugin.js(transform handler entry) andcreate-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 andcompile()runs normally.In our project,
saved-views.server.tshappens to keep onecreateServerFn({ method: 'GET' }).handler(\nadjacent (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.tsand 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:src/example.server.ts:Start dev server.
curl http://localhost:3000/src/example.server.ts:.handler(...)replaced withcreateClientRpc(...)stub; helper body removed by dead-code elimination.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):console.log/console.errorin any code outside handler bodies?.inputValidator(...)in the chain?The
console.log/errorrequirement is because devtools-vite'senhanceConsoleLogonly AST-roundtrips files that matchconsole\.and containlog/error(perenhance-logs.ts). TheinputValidatorrequirement is because Babel's generator only splits the chain across lines when it's long enough — a barecreateServerFn().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 localdist/esm/create-server-fn-plugin/plugin.jswith this change and re-ran withenhancedLogs: { enabled: true }: all three of.server.tsfiles in our app transform cleanly (.handler(...)→createClientRpc(...)stubs, server-only imports stripped, noBuffererrors).Workaround
If you can't patch
start-plugin-core, disable the AST-roundtripping pre-plugin. For us that's:Cost: lose the source-URL annotation on
console.log/console.erroroutput in dev. JSXdata-tsd-sourceinjection (a separate plugin indevtools-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'senhanceConsoleLogis just the most likely encounter today, but anyone writing a Babel plugin that touches.server.tsfiles (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.tsfile 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.