Describe the bug
In build mode, when createServerFn's static analysis (the start-compiler-plugin) needs to recursively load a module to detect what it references, and that module resolves to import-protection's mock-edge virtual module (i.e. some import in the dependency chain is denied in the client environment and gets swapped for a generated mock), the build crashes with:
[tanstack-start-core::server-fn:client] Could not load tanstack-start-import-protection:mock-edge:<base64>: ENOENT: no such file or directory, open 'tanstack-start-import-protection:mock-edge:<base64>'
Root cause
In packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts, the resolveId callback passed into StartCompiler does:
resolveId: async (source, importer) => {
const r = await this.resolve(source, importer)
if (r) {
if (!r.external) return cleanId(r.id)
}
return null
}
cleanId() strips the leading \0 that marks a Rollup-internal virtual module id. That's correct for normal filesystem-backed ids, but when r.id is one of import-protection's own virtual ids (e.g. \0tanstack-start-import-protection:mock-edge:...), stripping the \0 produces a bare string like tanstack-start-import-protection:mock-edge:....
That cleaned id is then passed to loadModule, which in build mode calls:
const loaded = await this.load({ id: id2 })
Rollup's load hook dispatch for import-protection's own load handler filters on id: new RegExp(getResolvedVirtualModuleMatchers().map(escapeRegExp).join("|")), and every entry in getResolvedVirtualModuleMatchers() is \0-prefixed. Since the id passed to this.load() here lost its \0 prefix, the filter never matches, so Rollup falls through to vite:load-fallback, which tries to literally fs.readFile() the string and throws ENOENT.
Reproduction
Minimal repro: a basic TanStack Start app (native-Vite mode, not vinxi) with a single route that imports createServerFn and calls it in a route loader, e.g.:
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
const getData = createServerFn({ method: "GET" }).handler(async () => {
const mod = await import("some-package-with-a-denied-import-in-its-dependency-chain");
return { result: mod.compute() };
});
export const Route = createFileRoute("/")({
loader: () => getData(),
component: () => null,
});
npm run build (vite build) fails with the ENOENT error above whenever the imported dependency's chain contains something import-protection would deny in the client environment (e.g. a Node builtin import several levels deep). We reproduced this on both the first version to ship import-protection (@tanstack/react-start@1.165.0 / @tanstack/start-plugin-core@1.165.0) and today's latest (@tanstack/react-start@1.168.27 / @tanstack/start-plugin-core@1.171.19) — it reproduces across the entire version range that has import-protection.
Workaround
We patched resolveId to skip cleanId specifically for import-protection's own virtual ids:
resolveId: async (source, importer) => {
const r = await this.resolve(source, importer)
if (r) {
- if (!r.external) return cleanId(r.id)
+ if (!r.external) return r.id.startsWith("\0tanstack-start-import-protection:") ? r.id : cleanId(r.id)
}
return null
}
This fixes the build without disabling import-protection — the mock module resolves and loads correctly, exactly as it's designed to.
Your Example Website or App
N/A — minimal repro described above, happy to provide a full reproduction repo if useful.
Steps to Reproduce the Bug or Issue
- Scaffold a TanStack Start app using native-Vite mode (not vinxi) with
@tanstack/react-start >= 1.165.0.
- Add a
createServerFn().handler() whose handler references (even transitively) something import-protection would mark as a denied import in the client environment.
- Run
vite build.
- Observe the
Could not load tanstack-start-import-protection:mock-edge:...: ENOENT crash.
Expected behavior
The build should succeed, with the denied import correctly replaced by import-protection's generated mock module (as it does for the analogous cases that don't happen to route through the server-fn compiler's internal resolveId/loadModule static-analysis path).
Screenshots or Videos
No response
Platform
- OS: Linux (NixOS 25.11)
- Node: v22 / v26
- @tanstack/react-start: 1.165.0 through 1.168.27 (reproduces across this whole range)
- vite: 7.3.6
- Build mode: native Vite (not vinxi)
Additional context
Related-but-distinct: the older pre-import-protection bug class (#4022, #3990, #2783 — "Readable is not exported by __vite-browser-external") is still present in versions before import-protection shipped (confirmed at 1.144.0/1.145.4 and 1.160.0). So currently there's no published version of @tanstack/react-start that builds a createServerFn-using native-Vite app cleanly without either this bug or that one, unless the fix above (or something equivalent) is applied.
Validations
Describe the bug
In build mode, when
createServerFn's static analysis (thestart-compiler-plugin) needs to recursively load a module to detect what it references, and that module resolves to import-protection'smock-edgevirtual module (i.e. some import in the dependency chain is denied in the client environment and gets swapped for a generated mock), the build crashes with:Root cause
In
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts, theresolveIdcallback passed intoStartCompilerdoes:cleanId()strips the leading\0that marks a Rollup-internal virtual module id. That's correct for normal filesystem-backed ids, but whenr.idis one of import-protection's own virtual ids (e.g.\0tanstack-start-import-protection:mock-edge:...), stripping the\0produces a bare string liketanstack-start-import-protection:mock-edge:....That cleaned id is then passed to
loadModule, which in build mode calls:Rollup's
loadhook dispatch for import-protection's ownloadhandler filters onid: new RegExp(getResolvedVirtualModuleMatchers().map(escapeRegExp).join("|")), and every entry ingetResolvedVirtualModuleMatchers()is\0-prefixed. Since the id passed tothis.load()here lost its\0prefix, the filter never matches, so Rollup falls through tovite:load-fallback, which tries to literallyfs.readFile()the string and throwsENOENT.Reproduction
Minimal repro: a basic TanStack Start app (native-Vite mode, not vinxi) with a single route that imports
createServerFnand calls it in a routeloader, e.g.:npm run build(vite build) fails with the ENOENT error above whenever the imported dependency's chain contains something import-protection would deny in the client environment (e.g. a Node builtin import several levels deep). We reproduced this on both the first version to ship import-protection (@tanstack/react-start@1.165.0/@tanstack/start-plugin-core@1.165.0) and today's latest (@tanstack/react-start@1.168.27/@tanstack/start-plugin-core@1.171.19) — it reproduces across the entire version range that has import-protection.Workaround
We patched
resolveIdto skipcleanIdspecifically for import-protection's own virtual ids:resolveId: async (source, importer) => { const r = await this.resolve(source, importer) if (r) { - if (!r.external) return cleanId(r.id) + if (!r.external) return r.id.startsWith("\0tanstack-start-import-protection:") ? r.id : cleanId(r.id) } return null }This fixes the build without disabling import-protection — the mock module resolves and loads correctly, exactly as it's designed to.
Your Example Website or App
N/A — minimal repro described above, happy to provide a full reproduction repo if useful.
Steps to Reproduce the Bug or Issue
@tanstack/react-start>= 1.165.0.createServerFn().handler()whose handler references (even transitively) something import-protection would mark as a denied import in the client environment.vite build.Could not load tanstack-start-import-protection:mock-edge:...: ENOENTcrash.Expected behavior
The build should succeed, with the denied import correctly replaced by import-protection's generated mock module (as it does for the analogous cases that don't happen to route through the server-fn compiler's internal
resolveId/loadModulestatic-analysis path).Screenshots or Videos
No response
Platform
Additional context
Related-but-distinct: the older pre-import-protection bug class (#4022, #3990, #2783 — "Readable is not exported by
__vite-browser-external") is still present in versions before import-protection shipped (confirmed at 1.144.0/1.145.4 and 1.160.0). So currently there's no published version of@tanstack/react-startthat builds acreateServerFn-using native-Vite app cleanly without either this bug or that one, unless the fix above (or something equivalent) is applied.Validations