diff --git a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts index 0c64052f6c..de22b726ed 100644 --- a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts +++ b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts @@ -114,6 +114,10 @@ export async function getRouteNodes( `${dir}/${removeExt(node.filePath)}`, ) node.routePath = routePath + // Keep originalRoutePath aligned with routePath for escape detection + if (node.originalRoutePath) { + node.originalRoutePath = `/${dir}${node.originalRoutePath}` + } node.filePath = filePath }) diff --git a/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts b/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts index 35e35d85e6..920e066ab1 100644 --- a/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts +++ b/packages/router-generator/src/filesystem/virtual/getRouteNodes.ts @@ -1,5 +1,6 @@ import path, { join, resolve } from 'node:path' import { + determineInitialRoutePath, removeExt, removeLeadingSlash, removeTrailingSlash, @@ -156,6 +157,10 @@ export async function getRouteNodesRecursive( `${node.pathPrefix}/${removeExt(subtreeNode.filePath)}`, ) subtreeNode.routePath = `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.routePath}` + // Keep originalRoutePath aligned with routePath for escape detection + if (subtreeNode.originalRoutePath) { + subtreeNode.originalRoutePath = `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.originalRoutePath}` + } subtreeNode.filePath = `${node.directory}/${subtreeNode.filePath}` }) return routeNodes @@ -186,7 +191,15 @@ export async function getRouteNodesRecursive( const lastSegment = node.path let routeNode: RouteNode - const routePath = `${parentRoutePath}/${removeLeadingSlash(lastSegment)}` + // Process the segment to handle escape sequences like [_] + const { + routePath: escapedSegment, + originalRoutePath: originalSegment, + } = determineInitialRoutePath(removeLeadingSlash(lastSegment)) + const routePath = `${parentRoutePath}${escapedSegment}` + // Store the original path with brackets for escape detection + const originalRoutePath = `${parentRoutePath}${originalSegment}` + if (node.file) { const { filePath, variableName, fullPath } = getFile(node.file) routeNode = { @@ -194,6 +207,7 @@ export async function getRouteNodesRecursive( fullPath, variableName, routePath, + originalRoutePath, _fsRouteType: 'static', } } else { @@ -202,6 +216,7 @@ export async function getRouteNodesRecursive( fullPath: '', variableName: routePathToVariable(routePath), routePath, + originalRoutePath, isVirtual: true, _fsRouteType: 'static', } @@ -235,13 +250,21 @@ export async function getRouteNodesRecursive( node.id = ensureLeadingUnderScore(fileNameWithoutExt) } const lastSegment = node.id - const routePath = `${parentRoutePath}/${removeLeadingSlash(lastSegment)}` + // Process the segment to handle escape sequences like [_] + const { + routePath: escapedSegment, + originalRoutePath: originalSegment, + } = determineInitialRoutePath(removeLeadingSlash(lastSegment)) + const routePath = `${parentRoutePath}${escapedSegment}` + // Store the original path with brackets for escape detection + const originalRoutePath = `${parentRoutePath}${originalSegment}` const routeNode: RouteNode = { fullPath, filePath, variableName, routePath, + originalRoutePath, _fsRouteType: 'pathless_layout', } diff --git a/packages/router-generator/tests/generator.test.ts b/packages/router-generator/tests/generator.test.ts index 13aa39db3a..42594d7b0c 100644 --- a/packages/router-generator/tests/generator.test.ts +++ b/packages/router-generator/tests/generator.test.ts @@ -104,6 +104,17 @@ function rewriteConfigByFolderName(folderName: string, config: Config) { case 'virtual-config-file-default-export': config.virtualRouteConfig = './routes.ts' break + case 'virtual-with-escaped-underscore': + { + // Test case for escaped underscores in physical routes mounted via virtual config + // This ensures originalRoutePath is correctly prefixed when paths are updated + const virtualRouteConfig = rootRoute('__root.tsx', [ + index('index.tsx'), + physical('/api', 'physical-routes'), + ]) + config.virtualRouteConfig = virtualRouteConfig + } + break case 'types-disabled': config.disableTypes = true config.generatedRouteTree = diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts new file mode 100644 index 0000000000..53ba374b1a --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routeTree.snapshot.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as nestedCallbackRouteImport } from './routes/nested/callback' +import { Route as nestedAuthRouteImport } from './routes/nested/auth' +import { Route as nestedHomeRouteImport } from './routes/nested/home' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const nestedCallbackRoute = nestedCallbackRouteImport.update({ + id: '/nested/_callback', + path: '/nested/_callback', + getParentRoute: () => rootRouteImport, +} as any) +const nestedAuthRoute = nestedAuthRouteImport.update({ + id: '/nested/_auth', + path: '/nested/_auth', + getParentRoute: () => rootRouteImport, +} as any) +const nestedHomeRoute = nestedHomeRouteImport.update({ + id: '/nested/', + path: '/nested/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/nested': typeof nestedHomeRoute + '/nested/_auth': typeof nestedAuthRoute + '/nested/_callback': typeof nestedCallbackRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/nested': typeof nestedHomeRoute + '/nested/_auth': typeof nestedAuthRoute + '/nested/_callback': typeof nestedCallbackRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/nested/': typeof nestedHomeRoute + '/nested/_auth': typeof nestedAuthRoute + '/nested/_callback': typeof nestedCallbackRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/nested' | '/nested/_auth' | '/nested/_callback' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/nested' | '/nested/_auth' | '/nested/_callback' + id: '__root__' | '/' | '/nested/' | '/nested/_auth' | '/nested/_callback' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + nestedHomeRoute: typeof nestedHomeRoute + nestedAuthRoute: typeof nestedAuthRoute + nestedCallbackRoute: typeof nestedCallbackRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/_callback': { + id: '/nested/_callback' + path: '/nested/_callback' + fullPath: '/nested/_callback' + preLoaderRoute: typeof nestedCallbackRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/_auth': { + id: '/nested/_auth' + path: '/nested/_auth' + fullPath: '/nested/_auth' + preLoaderRoute: typeof nestedAuthRouteImport + parentRoute: typeof rootRouteImport + } + '/nested/': { + id: '/nested/' + path: '/nested' + fullPath: '/nested' + preLoaderRoute: typeof nestedHomeRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + nestedHomeRoute: nestedHomeRoute, + nestedAuthRoute: nestedAuthRoute, + nestedCallbackRoute: nestedCallbackRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/__root.tsx b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/__root.tsx new file mode 100644 index 0000000000..87099187f8 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/__root.tsx @@ -0,0 +1,2 @@ +import { createRootRoute } from '@tanstack/react-router' +export const Route = createRootRoute() diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/index.tsx b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/index.tsx new file mode 100644 index 0000000000..d1d6296bab --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/index.tsx @@ -0,0 +1,2 @@ +import { createFileRoute } from '@tanstack/react-router' +export const Route = createFileRoute('/')() diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/__virtual.ts b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/__virtual.ts new file mode 100644 index 0000000000..391b0a3da0 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/__virtual.ts @@ -0,0 +1,11 @@ +import { + defineVirtualSubtreeConfig, + index, + route, +} from '@tanstack/virtual-file-routes' + +export default defineVirtualSubtreeConfig([ + index('home.tsx'), + route('[_]auth', 'auth.tsx'), + route('[_]callback', 'callback.tsx'), +]) diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/auth.tsx b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/auth.tsx new file mode 100644 index 0000000000..0da5aace4c --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/auth.tsx @@ -0,0 +1,2 @@ +import { createFileRoute } from '@tanstack/react-router' +export const Route = createFileRoute('/nested/_auth')() diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/callback.tsx b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/callback.tsx new file mode 100644 index 0000000000..8885ecd48e --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/callback.tsx @@ -0,0 +1,2 @@ +import { createFileRoute } from '@tanstack/react-router' +export const Route = createFileRoute('/nested/_callback')() diff --git a/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/home.tsx b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/home.tsx new file mode 100644 index 0000000000..28c3b04574 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-inside-with-escaped-underscore/routes/nested/home.tsx @@ -0,0 +1,2 @@ +import { createFileRoute } from '@tanstack/react-router' +export const Route = createFileRoute('/nested/')() diff --git a/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routeTree.snapshot.ts new file mode 100644 index 0000000000..5726099cc7 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routeTree.snapshot.ts @@ -0,0 +1,114 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as indexRouteImport } from './routes/index' +import { Route as ApiIndexRouteImport } from './routes/physical-routes/index' +import { Route as ApiChar91_Char93helloRouteImport } from './routes/physical-routes/[_]hello' +import { Route as ApiChar91_Char93authDotrouteRouteImport } from './routes/physical-routes/[_]auth.route' + +const indexRoute = indexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiIndexRoute = ApiIndexRouteImport.update({ + id: '/api/', + path: '/api/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiChar91_Char93helloRoute = ApiChar91_Char93helloRouteImport.update({ + id: '/api/_hello', + path: '/api/_hello', + getParentRoute: () => rootRouteImport, +} as any) +const ApiChar91_Char93authDotrouteRoute = + ApiChar91_Char93authDotrouteRouteImport.update({ + id: '/api/_auth', + path: '/api/_auth', + getParentRoute: () => rootRouteImport, + } as any) + +export interface FileRoutesByFullPath { + '/': typeof indexRoute + '/api/_auth': typeof ApiChar91_Char93authDotrouteRoute + '/api/_hello': typeof ApiChar91_Char93helloRoute + '/api': typeof ApiIndexRoute +} +export interface FileRoutesByTo { + '/': typeof indexRoute + '/api/_auth': typeof ApiChar91_Char93authDotrouteRoute + '/api/_hello': typeof ApiChar91_Char93helloRoute + '/api': typeof ApiIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof indexRoute + '/api/_auth': typeof ApiChar91_Char93authDotrouteRoute + '/api/_hello': typeof ApiChar91_Char93helloRoute + '/api/': typeof ApiIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/_auth' | '/api/_hello' | '/api' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/_auth' | '/api/_hello' | '/api' + id: '__root__' | '/' | '/api/_auth' | '/api/_hello' | '/api/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + indexRoute: typeof indexRoute + ApiChar91_Char93authDotrouteRoute: typeof ApiChar91_Char93authDotrouteRoute + ApiChar91_Char93helloRoute: typeof ApiChar91_Char93helloRoute + ApiIndexRoute: typeof ApiIndexRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof indexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/': { + id: '/api/' + path: '/api' + fullPath: '/api' + preLoaderRoute: typeof ApiIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/_hello': { + id: '/api/_hello' + path: '/api/_hello' + fullPath: '/api/_hello' + preLoaderRoute: typeof ApiChar91_Char93helloRouteImport + parentRoute: typeof rootRouteImport + } + '/api/_auth': { + id: '/api/_auth' + path: '/api/_auth' + fullPath: '/api/_auth' + preLoaderRoute: typeof ApiChar91_Char93authDotrouteRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + indexRoute: indexRoute, + ApiChar91_Char93authDotrouteRoute: ApiChar91_Char93authDotrouteRoute, + ApiChar91_Char93helloRoute: ApiChar91_Char93helloRoute, + ApiIndexRoute: ApiIndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/__root.tsx b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/__root.tsx new file mode 100644 index 0000000000..9c657c7d5b --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/__root.tsx @@ -0,0 +1,3 @@ +import { createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute() diff --git a/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/index.tsx b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/index.tsx new file mode 100644 index 0000000000..f077c2f494 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: () => 'Hello', +}) diff --git a/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/[_]auth.route.tsx b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/[_]auth.route.tsx new file mode 100644 index 0000000000..9c3be05246 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/[_]auth.route.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/_auth')({ + component: () => 'Auth Route', +}) diff --git a/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/[_]hello.tsx b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/[_]hello.tsx new file mode 100644 index 0000000000..a41e9110cf --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/[_]hello.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/_hello')({ + component: () => 'Hello API', +}) diff --git a/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/index.tsx b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/index.tsx new file mode 100644 index 0000000000..b5e43f6788 --- /dev/null +++ b/packages/router-generator/tests/generator/virtual-with-escaped-underscore/routes/physical-routes/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/')({ + component: () => 'API Index', +})