Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fix-code-splitter-quote-escaping.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 9 additions & 2 deletions packages/router-plugin/src/core/code-splitter/compilers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -623,7 +630,7 @@ export function compileCodeSplitReferenceRoute(
) {
programPath.unshiftContainer('body', [
template.statement(
`const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`,
`const ${splitNodeMeta.localImporterIdent} = () => import('${escapeSingleQuotedString(splitUrl)}')`,
)(),
])
}
Expand Down Expand Up @@ -719,7 +726,7 @@ export function compileCodeSplitReferenceRoute(
) {
programPath.unshiftContainer('body', [
template.statement(
`const ${splitNodeMeta.localImporterIdent} = () => import('${splitUrl}')`,
`const ${splitNodeMeta.localImporterIdent} = () => import('${escapeSingleQuotedString(splitUrl)}')`,
)(),
])
}
Expand Down
69 changes: 69 additions & 0 deletions packages/router-plugin/tests/code-splitter-path-escaping.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
const ast = parseAst({ code })
const urls: Array<string> = []

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`,
)
})
})
})