From f2b8e8174784e2bebedb3d1c4bd478b3eedb5615 Mon Sep 17 00:00:00 2001 From: thribhuvan003 Date: Thu, 9 Jul 2026 00:48:05 +0530 Subject: [PATCH 1/2] fix(router-plugin): escape single quotes in code-split importer paths --- .../src/core/code-splitter/compilers.ts | 11 ++- .../tests/code-splitter-path-escaping.test.ts | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 packages/router-plugin/tests/code-splitter-path-escaping.test.ts diff --git a/packages/router-plugin/src/core/code-splitter/compilers.ts b/packages/router-plugin/src/core/code-splitter/compilers.ts index 9411165630..f8f9ce3ff0 100644 --- a/packages/router-plugin/src/core/code-splitter/compilers.ts +++ b/packages/router-plugin/src/core/code-splitter/compilers.ts @@ -124,6 +124,13 @@ function removeSplitSearchParamFromFilename(filename: string) { return bareFilename! } +// Escapes a value so it can be safely interpolated into a single-quoted +// string literal, e.g. filenames containing `'` would otherwise produce +// unparsable code (#7754) +function escapeSingleQuotedString(value: string) { + return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'") +} + export function addSharedSearchParamToFilename(filename: string) { const [bareFilename] = filename.split('?') return `${bareFilename}?${tsrShared}=1` @@ -623,7 +630,7 @@ export function compileCodeSplitReferenceRoute( ) { programPath.unshiftContainer('body', [ template.statement( - `const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`, + `const ${splitNodeMeta.localImporterIdent} = () => import('${escapeSingleQuotedString(splitUrl)}')`, )(), ]) } @@ -719,7 +726,7 @@ export function compileCodeSplitReferenceRoute( ) { programPath.unshiftContainer('body', [ template.statement( - `const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`, + `const ${splitNodeMeta.localImporterIdent} = () => import('${escapeSingleQuotedString(splitUrl)}')`, )(), ]) } diff --git a/packages/router-plugin/tests/code-splitter-path-escaping.test.ts b/packages/router-plugin/tests/code-splitter-path-escaping.test.ts new file mode 100644 index 0000000000..cb5c47d003 --- /dev/null +++ b/packages/router-plugin/tests/code-splitter-path-escaping.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import { parseAst } from '@tanstack/router-utils' + +import { compileCodeSplitReferenceRoute } from '../src/core/code-splitter/compilers' +import { defaultCodeSplitGroupings } from '../src/core/constants' +import { getReferenceRouteCompilerPlugins } from '../src/core/code-splitter/plugins/framework-plugins' +import { frameworks } from './constants' + +/** + * Collects the string argument of every generated split-route importer, + * i.e. `const $$splitXImporter = () => import('...')`, from compiled code. + */ +function getImporterUrls(code: string): Array { + const ast = parseAst({ code }) + const urls: Array = [] + + for (const statement of ast.program.body) { + if (statement.type !== 'VariableDeclaration') continue + for (const declaration of statement.declarations) { + const init = declaration.init + if ( + init?.type === 'ArrowFunctionExpression' && + init.body.type === 'CallExpression' && + init.body.callee.type === 'Import' && + init.body.arguments[0]?.type === 'StringLiteral' + ) { + urls.push(init.body.arguments[0].value) + } + } + } + + return urls +} + +// https://github.com/TanStack/router/issues/7754 +describe('code-splitter handles special characters in the file path', () => { + describe.each(frameworks)('FRAMEWORK=%s', (framework) => { + it('compiles a route whose absolute path contains a single quote', () => { + const filename = "/Users/dev/it's a repro/app/src/routes/index.tsx" + const code = ` +import { createFileRoute } from '@tanstack/${framework}-router' + +export const Route = createFileRoute('/')({ + component: () => 'Hello', +}) +` + + // Without escaping, this throws while parsing the generated + // `import('...')` statement, since `'` terminates the literal early + const compileResult = compileCodeSplitReferenceRoute({ + code, + filename, + id: filename, + addHmr: false, + codeSplitGroupings: defaultCodeSplitGroupings, + targetFramework: framework, + compilerPlugins: getReferenceRouteCompilerPlugins({ + targetFramework: framework, + addHmr: false, + }), + }) + + // The importer URL must round-trip the filename exactly + expect(getImporterUrls(compileResult.code)).toContain( + `${filename}?tsr-split=component`, + ) + }) + }) +}) From af932d27e5e32e283a707ee836a034027cac7f7e Mon Sep 17 00:00:00 2001 From: thribhuvan003 Date: Thu, 9 Jul 2026 01:00:45 +0530 Subject: [PATCH 2/2] chore: add changeset --- .changeset/fix-code-splitter-quote-escaping.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/fix-code-splitter-quote-escaping.md diff --git a/.changeset/fix-code-splitter-quote-escaping.md b/.changeset/fix-code-splitter-quote-escaping.md new file mode 100644 index 0000000000..187f6c28c6 --- /dev/null +++ b/.changeset/fix-code-splitter-quote-escaping.md @@ -0,0 +1,9 @@ +--- +'@tanstack/router-plugin': patch +--- + +fix(router-plugin): escape single quotes in code-split importer paths + +The code splitter now escapes `'` and `\` when interpolating the split url into the generated `import('...')` statement, so projects whose absolute path contains an apostrophe (e.g. `/Users/dev/it's a repro/...`) compile instead of failing to parse on every route. + +Fixes #7754.