Code-splitter breaks every route when the project path contains a single quote/apostrophe (`autoCodeSplitting: false` does not avoid it)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
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.jsondeps:@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, thencurl http://localhost:<port>/.- To confirm the config flag does not help, set
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:
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-core1.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
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 by locating the source counterparts of the mentioned code-splitter compilers.js and start-router-plugin/plugin.js files, then trace how filesystem paths become generated dynamic imports and how autoCodeSplitting reaches the Vite plugin list. Done means apostrophes in project paths no longer break route requests, and setting autoCodeSplitting to false prevents the code-splitter plugins from being wired in; add or run the relevant existing tests if available.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, vite
- Domain
- build-system, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100