Which project does this relate to?
Router (@tanstack/router-plugin) — with a secondary defect in Start (@tanstack/start-plugin-core)
Describe the bug
When a TanStack Start (or plain TanStack Router + Vite) project lives under an absolute path that contains a single quote / apostrophe (e.g. a macOS username directory, an external contributor's "it's" folder, etc.), every route request 500s.
@tanstack/router-plugin's code-splitter interpolates each route file's absolute path directly into a single-quoted JS string template without escaping it:
// node_modules/@tanstack/router-plugin/dist/esm/core/code-splitter/compilers.js
`const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`
If splitUrl contains a ', the generated code is syntactically invalid — the apostrophe terminates the string literal early — and Babel throws a parse error, which surfaces to the user as an unhandled 500 on every route.
This is not avoidable via configuration. Setting router: { autoCodeSplitting: false } in the tanstackStart() / tanstackRouter() plugin options does not prevent the crash, because @tanstack/start-plugin-core wires the code-splitter Vite plugins unconditionally — it never reads autoCodeSplitting before deciding whether to include them in the plugin list. See Root Cause below.
Minimal repro steps
- Create a project directory whose absolute path contains a single quote, e.g.:
mkdir -p "/Users/dev/it's a repro/app/src/routes"
cd "/Users/dev/it's a repro/app"
- Minimal
package.json deps: @tanstack/react-start, @tanstack/react-router, react, react-dom, vite, @vitejs/plugin-react.
vite.config.ts:
import { defineConfig } from 'vite'
import viteReact from '@vitejs/plugin-react'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
export default defineConfig({
plugins: [tanstackStart(), viteReact()],
})
src/routes/__root.tsx — standard root route with <Outlet /> and <Scripts />.
src/routes/index.tsx:
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({
component: () => <div>Hello</div>,
})
npm install && npx vite dev, then curl http://localhost:<port>/.
- To confirm the config flag does not help, set
router: { autoCodeSplitting: false } in tanstackStart({...}) and repeat step 6 — the failure is identical.
Expected vs Actual
Expected: the dev server compiles the route and returns the rendered page (HTTP 200), regardless of what characters appear in the absolute project path.
Actual: every request 500s. The dev server logs a Babel template parse error caused by the unescaped apostrophe splitting the generated import(...) string literal:
SyntaxError: Unexpected token, expected "," (2:220)
1 | /* @babel/template */;
> 2 | const $$splitComponentImporter = () => import('/Users/dev/it's a repro/app/src/routes/index.tsx?tsr-split=component')
| ^
code: 'BABEL_TEMPLATE_PARSE_ERROR',
reasonCode: 'UnexpectedToken',
plugin: 'tanstack-router:code-splitter:compile-reference-file',
id: "/Users/dev/it's a repro/app/src/routes/index.tsx",
with the stack trace bottoming out at:
at node_modules/@tanstack/router-plugin/dist/esm/core/code-splitter/compilers.js:299:128
at Array.forEach (<anonymous>)
at babelHandleReference (.../compilers.js:274:30)
at CallExpression (.../compilers.js:345:50)
HTTP response: 500 Internal Server Error, body {"status":500,"unhandled":true,"message":"HTTPError"}.
Setting router: { autoCodeSplitting: false } produces the exact same error and stack trace — the flag has no effect on this failure.
Root cause
Two separate defects compound:
1. Unescaped path interpolation in the code-splitter (@tanstack/router-plugin 1.168.19)
node_modules/@tanstack/router-plugin/dist/esm/core/code-splitter/compilers.js, inside compileCodeSplitReferenceRoute, two call sites build the dynamic-import statement by interpolating splitUrl (an absolute filesystem path + ?tsr-split=... query) into a single-quoted template string, with no escaping:
- Line 299:
if (!hasImportedOrDefinedIdentifier(splitNodeMeta.localImporterIdent)) programPath.unshiftContainer("body", [template.statement(`const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`)()]);
- Line 335 (same pattern, second branch of the same function):
if (!hasImportedOrDefinedIdentifier(splitNodeMeta.localImporterIdent)) programPath.unshiftContainer("body", [template.statement(`const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`)()]);
Any character in splitUrl that terminates a single-quoted JS string (') — or otherwise isn't valid inside one — breaks the generated source.
2. autoCodeSplitting: false is not honored by @tanstack/start-plugin-core
node_modules/@tanstack/start-plugin-core/dist/esm/vite/start-router-plugin/plugin.js, function tanStackStartRouter (around lines 91–133), unconditionally returns an array that includes two tanStackRouterCodeSplitter(...) plugin instances (one for the client environment, one for the server environment):
return [
clientTreePlugin,
tanstackRouterGenerator(() => { ... }, routerPluginContext),
tanStackRouterCodeSplitter(() => {
const routerConfig = getConfig().startConfig.router;
return { ...routerConfig, codeSplittingOptions: { ... }, plugin: { vite: { environmentName: VITE_ENVIRONMENT_NAMES.client } } };
}, routerPluginContext),
tanStackRouterCodeSplitter(() => {
const routerConfig = getConfig().startConfig.router;
return { ...routerConfig, codeSplittingOptions: { ... }, plugin: { vite: { environmentName: VITE_ENVIRONMENT_NAMES.server } } };
}, routerPluginContext)
]
routerConfig.autoCodeSplitting is spread into the returned options object but is never read to decide whether to push the tanStackRouterCodeSplitter(...) plugins into the array in the first place. So even with router: { autoCodeSplitting: false }, both code-splitter plugins are still instantiated and wired into the Vite pipeline, and defect 1 still fires.
Suggested fix
Escape the interpolated path with JSON.stringify() at both sites in compilers.js (verified locally — patching this eliminates the syntax error entirely; a route in an apostrophe-path project then compiles and progresses past this failure):
--- a/node_modules/@tanstack/router-plugin/dist/esm/core/code-splitter/compilers.js
+++ b/node_modules/@tanstack/router-plugin/dist/esm/core/code-splitter/compilers.js
@@ -296,7 +296,7 @@ function compileCodeSplitReferenceRoute(opts) {
if (!shouldSplit) return;
modified = true;
if (!hasImportedOrDefinedIdentifier(LAZY_ROUTE_COMPONENT_IDENT)) programPath.unshiftContainer("body", [template.statement(`import { ${LAZY_ROUTE_COMPONENT_IDENT} } from '${PACKAGE}'`)()]);
- if (!hasImportedOrDefinedIdentifier(splitNodeMeta.localImporterIdent)) programPath.unshiftContainer("body", [template.statement(`const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`)()]);
+ if (!hasImportedOrDefinedIdentifier(splitNodeMeta.localImporterIdent)) programPath.unshiftContainer("body", [template.statement(`const ${splitNodeMeta.localImporterIdent} = () => import(${JSON.stringify(splitUrl)})`)()]);
const insertionPath = path.getStatementParent() ?? path;
let splitPropValue;
for (const plugin of opts.compilerPlugins ?? []) {
@@ -332,7 +332,7 @@ function compileCodeSplitReferenceRoute(opts) {
if (!shouldSplit) return;
modified = true;
if (!hasImportedOrDefinedIdentifier(LAZY_FN_IDENT)) programPath.unshiftContainer("body", template.smart(`import { ${LAZY_FN_IDENT} } from '${PACKAGE}'`)());
- if (!hasImportedOrDefinedIdentifier(splitNodeMeta.localImporterIdent)) programPath.unshiftContainer("body", [template.statement(`const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`)()]);
+ if (!hasImportedOrDefinedIdentifier(splitNodeMeta.localImporterIdent)) programPath.unshiftContainer("body", [template.statement(`const ${splitNodeMeta.localImporterIdent} = () => import(${JSON.stringify(splitUrl)})`)()]);
prop.value = template.expression(`${LAZY_FN_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}')`)();
}
}
It's worth auditing the rest of compilers.js (and any sibling generator/compiler files that build source via string templates) for the same pattern — other interpolated identifiers/paths built from filesystem input are equally at risk.
Separately, for defect 2: tanStackStartRouter in start-plugin-core's start-router-plugin/plugin.js should check routerConfig.autoCodeSplitting before pushing the tanStackRouterCodeSplitter(...) entries into the returned plugin array, so the documented flag actually disables the code-splitter instead of only affecting its internal options.
Environment
@tanstack/react-start: 1.168.27 (latest at time of testing)
@tanstack/router-plugin: 1.168.19 (latest at time of testing — also the version pinned by the latest @tanstack/start-plugin-core 1.171.19)
@tanstack/react-router: 1.170.17
@tanstack/start-plugin-core: 1.171.19
- Vite: 7.3.6
- Node: v24.14.0
- OS: macOS (Darwin 25.5.0)
- Bundler: esbuild/Vite dev server (module runner), also expected to affect production builds since the same compiler path is shared
Which project does this relate to?
Router (
@tanstack/router-plugin) — with a secondary defect in Start (@tanstack/start-plugin-core)Describe the bug
When a TanStack Start (or plain TanStack Router + Vite) project lives under an absolute path that contains a single quote / apostrophe (e.g. a macOS username directory, an external contributor's "it's" folder, etc.), every route request 500s.
@tanstack/router-plugin's code-splitter interpolates each route file's absolute path directly into a single-quoted JS string template without escaping it:If
splitUrlcontains a', the generated code is syntactically invalid — the apostrophe terminates the string literal early — and Babel throws a parse error, which surfaces to the user as an unhandled 500 on every route.This is not avoidable via configuration. Setting
router: { autoCodeSplitting: false }in thetanstackStart()/tanstackRouter()plugin options does not prevent the crash, because@tanstack/start-plugin-corewires the code-splitter Vite plugins unconditionally — it never readsautoCodeSplittingbefore deciding whether to include them in the plugin list. See Root Cause below.Minimal repro steps
package.jsondeps:@tanstack/react-start,@tanstack/react-router,react,react-dom,vite,@vitejs/plugin-react.vite.config.ts:src/routes/__root.tsx— standard root route with<Outlet />and<Scripts />.src/routes/index.tsx:npm install && npx vite dev, thencurl http://localhost:<port>/.router: { autoCodeSplitting: false }intanstackStart({...})and repeat step 6 — the failure is identical.Expected vs Actual
Expected: the dev server compiles the route and returns the rendered page (HTTP 200), regardless of what characters appear in the absolute project path.
Actual: every request 500s. The dev server logs a Babel template parse error caused by the unescaped apostrophe splitting the generated
import(...)string literal:with the stack trace bottoming out at:
HTTP response:
500 Internal Server Error, body{"status":500,"unhandled":true,"message":"HTTPError"}.Setting
router: { autoCodeSplitting: false }produces the exact same error and stack trace — the flag has no effect on this failure.Root cause
Two separate defects compound:
1. Unescaped path interpolation in the code-splitter (
@tanstack/router-plugin1.168.19)node_modules/@tanstack/router-plugin/dist/esm/core/code-splitter/compilers.js, insidecompileCodeSplitReferenceRoute, two call sites build the dynamic-import statement by interpolatingsplitUrl(an absolute filesystem path +?tsr-split=...query) into a single-quoted template string, with no escaping:Any character in
splitUrlthat terminates a single-quoted JS string (') — or otherwise isn't valid inside one — breaks the generated source.2.
autoCodeSplitting: falseis not honored by@tanstack/start-plugin-corenode_modules/@tanstack/start-plugin-core/dist/esm/vite/start-router-plugin/plugin.js, functiontanStackStartRouter(around lines 91–133), unconditionally returns an array that includes twotanStackRouterCodeSplitter(...)plugin instances (one for the client environment, one for the server environment):routerConfig.autoCodeSplittingis spread into the returned options object but is never read to decide whether to push thetanStackRouterCodeSplitter(...)plugins into the array in the first place. So even withrouter: { autoCodeSplitting: false }, both code-splitter plugins are still instantiated and wired into the Vite pipeline, and defect 1 still fires.Suggested fix
Escape the interpolated path with
JSON.stringify()at both sites incompilers.js(verified locally — patching this eliminates the syntax error entirely; a route in an apostrophe-path project then compiles and progresses past this failure):It's worth auditing the rest of
compilers.js(and any sibling generator/compiler files that build source via string templates) for the same pattern — other interpolated identifiers/paths built from filesystem input are equally at risk.Separately, for defect 2:
tanStackStartRouterinstart-plugin-core'sstart-router-plugin/plugin.jsshould checkrouterConfig.autoCodeSplittingbefore pushing thetanStackRouterCodeSplitter(...)entries into the returned plugin array, so the documented flag actually disables the code-splitter instead of only affecting its internal options.Environment
@tanstack/react-start: 1.168.27 (latest at time of testing)@tanstack/router-plugin: 1.168.19 (latest at time of testing — also the version pinned by the latest@tanstack/start-plugin-core1.171.19)@tanstack/react-router: 1.170.17@tanstack/start-plugin-core: 1.171.19