Skip to content

[_] escape for leading underscore in path is dropped when route has a pathless layout ancestor (virtual file routes) #7393

@dmarcey

Description

@dmarcey

Which project does this relate to?

Router

Describe the bug

When a route's path uses the documented [_] escape to keep a literal leading underscore in the URL, the escape is silently ignored if any ancestor is a pathless layout (a route whose path starts with _). The leading underscore is stripped from the generated fullPath.

This reproduces with virtual file routes; I haven't tested file-based routing but the offending code (inferFullPath / removeUnderscoresWithEscape) is shared.

Complete minimal reproducer

https://stackblitz.com/github/dmarcey/tanstack-router-underscore-reproducer

Steps to Reproduce the Bug

// routes.ts
import {layout, rootRoute, route} from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  // ✅ Works: '/[_]foo' → URL '/_foo'
  route('/[_]foo', 'foo.tsx'),

  // ❌ Broken: '/[_]bar' under a pathless layout → URL '/bar'
  layout('_layout', 'layout.tsx', [
    route('/[_]bar', 'bar.tsx'),
  ]),
])

After running the generator, the routeTree.gen.ts FileRoutesByFullPath includes '/bar' for bar.tsx instead of the expected '/_bar'.

  1. Configure virtual file routes as above.
  2. Run the router generator (@tanstack/router-generator 1.166.41).
  3. Inspect the generated fullPath / path entries — the underscore is stripped from any [_]… segment that lives under a pathless layout.

I also reproduced this directly against the published utils:

import * as utils from '@tanstack/router-generator/dist/esm/utils.js'

// Top-level (correct)
utils.inferFullPath({ routePath: '/_foo', originalRoutePath: '/[_]foo' })
// → '/_foo'  ✅

// Nested under pathless layout (incorrect)
utils.inferFullPath({ routePath: '/_layout/_foo', originalRoutePath: '/_layout/[_]foo' })
// → '/foo'  ❌  (expected '/_foo')

If you're using the app in the stackblitz, you can observe that:

  1. There is a type error when trying to define a <Link /> with a to for _nested.
  2. Hard navigating directly to /_nested fails.

Expected behavior

'/[_]foo' should produce '/_foo' regardless of whether the route is nested under a pathless layout.

Screenshots or Videos

No response

Platform

  • @tanstack/router-generator: 1.166.41
  • @tanstack/virtual-file-routes: 1.161.7
  • Node: 24.8.0
  • OS: Ubuntu 24.04

Additional context

Here's my best guess at the underlying problem:

In inferFullPath (packages/router-generator/src/utils.ts):

const fullPath = removeGroups(
  removeUnderscoresWithEscape(
    removeLayoutSegmentsWithEscape(routeNode.routePath, routeNode.originalRoutePath),
    routeNode.originalRoutePath, // not adjusted to match
  ),
)

removeLayoutSegmentsWithEscape filters segments out of routePath, but removeUnderscoresWithEscape is then handed the unmodified originalRoutePath and uses segment array indices to look up the escape state:

return routeSegments.map((segment, i) => {
  const originalSegment = originalSegments[i] || ''
  const leadingEscaped = hasEscapedLeadingUnderscore(originalSegment)
  // …
  if (result.startsWith('_') && !leadingEscaped) result = result.slice(1)
})

After the layout segment is dropped from routeSegments, the indexes no longer align with originalSegments, so hasEscapedLeadingUnderscore is checked against the wrong segment and returns false.

A fix would be to either (a) also strip the corresponding original segments inside removeLayoutSegmentsWithEscape and pass the trimmed originalRoutePath along, or (b) align segments by content rather than by index.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions