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/virtual-pathless-layout-dotted-filename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/router-generator': patch
---

fix(router-generator): keep dotted virtual pathless layout filenames as one segment

`layout('pathless.layout.tsx')` was running the layout id through `determineInitialRoutePath`, which treats unescaped dots as path separators. That turned `_pathless.layout` into `/_pathless/layout` and mounted children under a real `/layout` URL segment. Dots in layout ids are now escaped so the id stays a single pathless segment and children keep their original paths (e.g. `/subpath`).

Fixes #7761.
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,21 @@ export async function getRouteNodesRecursive(
node.id = ensureLeadingUnderScore(fileNameWithoutExt)
}
const lastSegment = node.id
// Layout ids from filenames like `pathless.layout.tsx` become
// `_pathless.layout`. Those dots are part of the identifier, not
// path separators — if we pass them through determineInitialRoutePath
// raw, SPLIT_REGEX turns them into `/` and children mount under a
// real `/layout` URL segment (regression from 1.145.1, #7761).
// Escape unbracketed dots so the id stays one pathless segment.
const segmentForPath = removeLeadingSlash(lastSegment).replace(
/(?<!\[)\.(?!\])/g,
'[.]',
)
// Process the segment to handle escape sequences like [_]
const {
routePath: escapedSegment,
originalRoutePath: originalSegment,
} = determineInitialRoutePath(removeLeadingSlash(lastSegment))
} = determineInitialRoutePath(segmentForPath)
const routePath = `${parentRoutePath}${escapedSegment}`
// Store the original path with brackets for escape detection
const originalRoutePath = `${parentOriginalRoutePath}${originalSegment}`
Expand Down
14 changes: 14 additions & 0 deletions packages/router-generator/tests/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,20 @@ function rewriteConfigByFolderName(folderName: string, config: Config) {
config.virtualRouteConfig = virtualRouteConfig
}
break
case 'virtual-pathless-layout-dotted-filename':
{
// Issue #7761: layout() whose filename contains a dot (pathless.layout.tsx)
// must stay a single pathless segment so children keep their URL
// (/subpath), not mount under a leaked /layout segment.
const virtualRouteConfig = rootRoute('root.route.tsx', [
index('index.route.tsx'),
layout('pathless.layout.tsx', [
route('subpath', 'subpath.route.tsx'),
]),
])
config.virtualRouteConfig = virtualRouteConfig
}
break
case 'virtual-root-sibling-routes':
{
// Test case for issue #5431: Virtual routes that are siblings at the root level
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* 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.route'
import { Route as pathlessDotlayoutRouteImport } from './routes/pathless.layout'
import { Route as indexDotrouteRouteImport } from './routes/index.route'
import { Route as subpathDotrouteRouteImport } from './routes/subpath.route'

const pathlessDotlayoutRoute = pathlessDotlayoutRouteImport.update({
id: '/_pathless.layout',
getParentRoute: () => rootRouteImport,
} as any)
const indexDotrouteRoute = indexDotrouteRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const subpathDotrouteRoute = subpathDotrouteRouteImport.update({
id: '/subpath',
path: '/subpath',
getParentRoute: () => pathlessDotlayoutRoute,
} as any)

export interface FileRoutesByFullPath {
'/': typeof indexDotrouteRoute
'/subpath': typeof subpathDotrouteRoute
}
export interface FileRoutesByTo {
'/': typeof indexDotrouteRoute
'/subpath': typeof subpathDotrouteRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof indexDotrouteRoute
'/_pathless.layout': typeof pathlessDotlayoutRouteWithChildren
'/_pathless.layout/subpath': typeof subpathDotrouteRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/subpath'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/subpath'
id: '__root__' | '/' | '/_pathless.layout' | '/_pathless.layout/subpath'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
indexDotrouteRoute: typeof indexDotrouteRoute
pathlessDotlayoutRoute: typeof pathlessDotlayoutRouteWithChildren
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/_pathless.layout': {
id: '/_pathless.layout'
path: ''
fullPath: '/'
preLoaderRoute: typeof pathlessDotlayoutRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof indexDotrouteRouteImport
parentRoute: typeof rootRouteImport
}
'/_pathless.layout/subpath': {
id: '/_pathless.layout/subpath'
path: '/subpath'
fullPath: '/subpath'
preLoaderRoute: typeof subpathDotrouteRouteImport
parentRoute: typeof pathlessDotlayoutRoute
}
}
}

interface pathlessDotlayoutRouteChildren {
subpathDotrouteRoute: typeof subpathDotrouteRoute
}

const pathlessDotlayoutRouteChildren: pathlessDotlayoutRouteChildren = {
subpathDotrouteRoute: subpathDotrouteRoute,
}

const pathlessDotlayoutRouteWithChildren =
pathlessDotlayoutRoute._addFileChildren(pathlessDotlayoutRouteChildren)

const rootRouteChildren: RootRouteChildren = {
indexDotrouteRoute: indexDotrouteRoute,
pathlessDotlayoutRoute: pathlessDotlayoutRouteWithChildren,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
component: () => 'Index',
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_pathless.layout')({
component: () => 'PathlessLayout',
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createRootRoute } from '@tanstack/react-router'

export const Route = createRootRoute()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_pathless.layout/subpath')({
component: () => 'Subpath',
})