+ This route was created using [_]layout.tsx to escape the leading
+ underscore. It renders at the literal path /_layout instead of being a
+ pathless layout.
+
+
+ )
+}
diff --git a/e2e/react-router/escaped-special-strings/src/routes/[index].tsx b/e2e/react-router/escaped-special-strings/src/routes/[index].tsx
new file mode 100644
index 00000000000..64076bb343d
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/src/routes/[index].tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+// This file uses [index] escaping to create a literal /index route
+// instead of being treated as an index route for the parent
+export const Route = createFileRoute('/index')({
+ component: EscapedIndexComponent,
+})
+
+function EscapedIndexComponent() {
+ return (
+
+
Escaped Index Page
+
/index
+
+ This route was created using [index].tsx to escape the special "index"
+ token. It renders at the literal path /index instead of being the index
+ route.
+
+
+ )
+}
diff --git a/e2e/react-router/escaped-special-strings/src/routes/[lazy].tsx b/e2e/react-router/escaped-special-strings/src/routes/[lazy].tsx
new file mode 100644
index 00000000000..486244c2ade
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/src/routes/[lazy].tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+// This file uses [lazy] escaping to create a literal /lazy path
+// instead of being treated as a lazy-loaded route
+export const Route = createFileRoute('/lazy')({
+ component: EscapedLazyComponent,
+})
+
+function EscapedLazyComponent() {
+ return (
+
+
Escaped Lazy Page
+
/lazy
+
+ This route was created using [lazy].tsx to escape the special "lazy"
+ token. It renders at the literal path /lazy instead of being a
+ lazy-loaded route.
+
+
+ )
+}
diff --git a/e2e/react-router/escaped-special-strings/src/routes/[route].tsx b/e2e/react-router/escaped-special-strings/src/routes/[route].tsx
new file mode 100644
index 00000000000..f9e28fd87e5
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/src/routes/[route].tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+// This file uses [route] escaping to create a literal /route path
+// instead of being treated as a layout configuration file
+export const Route = createFileRoute('/route')({
+ component: EscapedRouteComponent,
+})
+
+function EscapedRouteComponent() {
+ return (
+
+
Escaped Route Page
+
/route
+
+ This route was created using [route].tsx to escape the special "route"
+ token. It renders at the literal path /route instead of being a layout
+ configuration.
+
+ )
+}
diff --git a/e2e/react-router/escaped-special-strings/src/routes/blog[_].tsx b/e2e/react-router/escaped-special-strings/src/routes/blog[_].tsx
new file mode 100644
index 00000000000..d3cbdf9d8c1
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/src/routes/blog[_].tsx
@@ -0,0 +1,21 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+// This file uses blog[_] escaping to create a literal /blog_ path
+// with the trailing underscore preserved in the URL
+export const Route = createFileRoute('/blog_')({
+ component: EscapedBlogUnderscoreComponent,
+})
+
+function EscapedBlogUnderscoreComponent() {
+ return (
+
+
Escaped Blog Underscore Page
+
/blog_
+
+ This route was created using blog[_].tsx to escape the trailing
+ underscore. It renders at the literal path /blog_ with the underscore
+ preserved in the URL.
+
+
+ )
+}
diff --git a/e2e/react-router/escaped-special-strings/tests/escaped-routes.spec.ts b/e2e/react-router/escaped-special-strings/tests/escaped-routes.spec.ts
new file mode 100644
index 00000000000..447b5c11945
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/tests/escaped-routes.spec.ts
@@ -0,0 +1,124 @@
+import { expect, test } from '@playwright/test'
+
+test.describe('Escaped special strings routing', () => {
+ test('escaped [index] route renders at /index path', async ({ page }) => {
+ await page.goto('/index')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Index Page',
+ )
+ await expect(page.getByTestId('page-path')).toContainText('/index')
+ await expect(page.getByTestId('page-description')).toContainText(
+ 'escape the special "index" token',
+ )
+ })
+
+ test('escaped [route] route renders at /route path', async ({ page }) => {
+ await page.goto('/route')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Route Page',
+ )
+ await expect(page.getByTestId('page-path')).toContainText('/route')
+ await expect(page.getByTestId('page-description')).toContainText(
+ 'escape the special "route" token',
+ )
+ })
+
+ test('escaped [lazy] route renders at /lazy path', async ({ page }) => {
+ await page.goto('/lazy')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Lazy Page',
+ )
+ await expect(page.getByTestId('page-path')).toContainText('/lazy')
+ await expect(page.getByTestId('page-description')).toContainText(
+ 'escape the special "lazy" token',
+ )
+ })
+
+ test('escaped [_]layout route renders at /_layout path', async ({ page }) => {
+ await page.goto('/_layout')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Underscore Layout Page',
+ )
+ await expect(page.getByTestId('page-path')).toContainText('/_layout')
+ await expect(page.getByTestId('page-description')).toContainText(
+ 'escape the leading underscore',
+ )
+ })
+
+ test('escaped blog[_] route renders at /blog_ path', async ({ page }) => {
+ await page.goto('/blog_')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Blog Underscore Page',
+ )
+ await expect(page.getByTestId('page-path')).toContainText('/blog_')
+ await expect(page.getByTestId('page-description')).toContainText(
+ 'escape the trailing underscore',
+ )
+ })
+
+ test('client-side navigation to escaped /index route', async ({ page }) => {
+ await page.goto('/route')
+ await page.getByTestId('link-index').click()
+ await page.waitForURL('/index')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Index Page',
+ )
+ })
+
+ test('client-side navigation to escaped /route route', async ({ page }) => {
+ await page.goto('/index')
+ await page.getByTestId('link-route').click()
+ await page.waitForURL('/route')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Route Page',
+ )
+ })
+
+ test('client-side navigation to escaped /lazy route', async ({ page }) => {
+ await page.goto('/index')
+ await page.getByTestId('link-lazy').click()
+ await page.waitForURL('/lazy')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Lazy Page',
+ )
+ })
+
+ test('client-side navigation to escaped /_layout route', async ({ page }) => {
+ await page.goto('/index')
+ await page.getByTestId('link-underscore-layout').click()
+ await page.waitForURL('/_layout')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Underscore Layout Page',
+ )
+ })
+
+ test('client-side navigation to escaped /blog_ route', async ({ page }) => {
+ await page.goto('/index')
+ await page.getByTestId('link-blog-underscore').click()
+ await page.waitForURL('/blog_')
+
+ await expect(page.getByTestId('page-title')).toContainText(
+ 'Escaped Blog Underscore Page',
+ )
+ })
+
+ test('URL is correct for escaped routes with underscores', async ({
+ page,
+ baseURL,
+ }) => {
+ await page.goto('/_layout')
+ expect(page.url()).toBe(`${baseURL}/_layout`)
+
+ await page.goto('/blog_')
+ expect(page.url()).toBe(`${baseURL}/blog_`)
+ })
+})
diff --git a/e2e/react-router/escaped-special-strings/tests/setup/global.setup.ts b/e2e/react-router/escaped-special-strings/tests/setup/global.setup.ts
new file mode 100644
index 00000000000..3593d10ab90
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/tests/setup/global.setup.ts
@@ -0,0 +1,6 @@
+import { e2eStartDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function setup() {
+ await e2eStartDummyServer(packageJson.name)
+}
diff --git a/e2e/react-router/escaped-special-strings/tests/setup/global.teardown.ts b/e2e/react-router/escaped-special-strings/tests/setup/global.teardown.ts
new file mode 100644
index 00000000000..62fd79911cc
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/tests/setup/global.teardown.ts
@@ -0,0 +1,6 @@
+import { e2eStopDummyServer } from '@tanstack/router-e2e-utils'
+import packageJson from '../../package.json' with { type: 'json' }
+
+export default async function teardown() {
+ await e2eStopDummyServer(packageJson.name)
+}
diff --git a/e2e/react-router/escaped-special-strings/tsconfig.json b/e2e/react-router/escaped-special-strings/tsconfig.json
new file mode 100644
index 00000000000..82cf0bcd2c9
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "allowJs": true,
+ "types": ["vite/client"]
+ },
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/e2e/react-router/escaped-special-strings/vite.config.js b/e2e/react-router/escaped-special-strings/vite.config.js
new file mode 100644
index 00000000000..940c8aa2363
--- /dev/null
+++ b/e2e/react-router/escaped-special-strings/vite.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ tanstackRouter({
+ target: 'react',
+ }),
+ react(),
+ ],
+})
diff --git a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts
index 8f137ebfa12..0c64052f6c4 100644
--- a/packages/router-generator/src/filesystem/physical/getRouteNodes.ts
+++ b/packages/router-generator/src/filesystem/physical/getRouteNodes.ts
@@ -2,6 +2,7 @@ import path from 'node:path'
import * as fsp from 'node:fs/promises'
import {
determineInitialRoutePath,
+ hasEscapedLeadingUnderscore,
removeExt,
replaceBackslash,
routePathToVariable,
@@ -153,7 +154,7 @@ export async function getRouteNodes(
throw new Error(errorMessage)
}
- const meta = getRouteMeta(routePath, config)
+ const meta = getRouteMeta(routePath, originalRoutePath, config)
const variableName = meta.variableName
let routeType: FsRouteType = meta.fsRouteType
@@ -164,7 +165,14 @@ export async function getRouteNodes(
// this check needs to happen after the lazy route has been cleaned up
// since the routePath is used to determine if a route is pathless
- if (isValidPathlessLayoutRoute(routePath, routeType, config)) {
+ if (
+ isValidPathlessLayoutRoute(
+ routePath,
+ originalRoutePath,
+ routeType,
+ config,
+ )
+ ) {
routeType = 'pathless_layout'
}
@@ -189,37 +197,64 @@ export async function getRouteNodes(
})
}
- routePath = routePath.replace(
- new RegExp(
- `/(component|errorComponent|notFoundComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
- ),
- '',
- )
-
- originalRoutePath = originalRoutePath.replace(
- new RegExp(
- `/(component|errorComponent|notFoundComponent|pendingComponent|loader|${config.routeToken}|lazy)$`,
- ),
- '',
- )
+ // Get the last segment of originalRoutePath to check for escaping
+ const originalSegments = originalRoutePath.split('/').filter(Boolean)
+ const lastOriginalSegmentForSuffix =
+ originalSegments[originalSegments.length - 1] || ''
+
+ // List of special suffixes that can be escaped
+ const specialSuffixes = [
+ 'component',
+ 'errorComponent',
+ 'notFoundComponent',
+ 'pendingComponent',
+ 'loader',
+ config.routeToken,
+ 'lazy',
+ ]
+
+ // Only strip the suffix if it wasn't escaped (not wrapped in brackets)
+ const suffixToStrip = specialSuffixes.find((suffix) => {
+ const endsWithSuffix = routePath.endsWith(`/${suffix}`)
+ const isEscaped = lastOriginalSegmentForSuffix === `[${suffix}]`
+ return endsWithSuffix && !isEscaped
+ })
- if (routePath === config.indexToken) {
- routePath = '/'
+ if (suffixToStrip) {
+ routePath = routePath.replace(new RegExp(`/${suffixToStrip}$`), '')
+ originalRoutePath = originalRoutePath.replace(
+ new RegExp(`/${suffixToStrip}$`),
+ '',
+ )
}
- if (originalRoutePath === config.indexToken) {
- originalRoutePath = '/'
+ // Check if the index token should be treated specially or as a literal path
+ // If it's escaped (wrapped in brackets in originalRoutePath), it should be literal
+ const lastOriginalSegment =
+ originalRoutePath.split('/').filter(Boolean).pop() || ''
+ const isIndexEscaped =
+ lastOriginalSegment === `[${config.indexToken}]`
+
+ if (!isIndexEscaped) {
+ if (routePath === config.indexToken) {
+ routePath = '/'
+ }
+
+ if (originalRoutePath === config.indexToken) {
+ originalRoutePath = '/'
+ }
+
+ routePath =
+ routePath.replace(new RegExp(`/${config.indexToken}$`), '/') ||
+ '/'
+
+ originalRoutePath =
+ originalRoutePath.replace(
+ new RegExp(`/${config.indexToken}$`),
+ '/',
+ ) || '/'
}
- routePath =
- routePath.replace(new RegExp(`/${config.indexToken}$`), '/') || '/'
-
- originalRoutePath =
- originalRoutePath.replace(
- new RegExp(`/${config.indexToken}$`),
- '/',
- ) || '/'
-
routeNodes.push({
filePath,
fullPath,
@@ -266,12 +301,14 @@ export async function getRouteNodes(
/**
* Determines the metadata for a given route path based on the provided configuration.
*
- * @param routePath - The determined initial routePath.
+ * @param routePath - The determined initial routePath (with brackets removed).
+ * @param originalRoutePath - The original route path (may contain brackets for escaped content).
* @param config - The user configuration object.
* @returns An object containing the type of the route and the variable name derived from the route path.
*/
export function getRouteMeta(
routePath: string,
+ originalRoutePath: string,
config: Pick,
): {
// `__root` is can be more easily determined by filtering down to routePath === /${rootPathId}
@@ -292,25 +329,50 @@ export function getRouteMeta(
} {
let fsRouteType: FsRouteType = 'static'
- if (routePath.endsWith(`/${config.routeToken}`)) {
+ // Get the last segment from the original path to check for escaping
+ const originalSegments = originalRoutePath.split('/').filter(Boolean)
+ const lastOriginalSegment =
+ originalSegments[originalSegments.length - 1] || ''
+
+ // Helper to check if a specific suffix is escaped
+ const isSuffixEscaped = (suffix: string): boolean => {
+ return lastOriginalSegment === `[${suffix}]`
+ }
+
+ if (
+ routePath.endsWith(`/${config.routeToken}`) &&
+ !isSuffixEscaped(config.routeToken)
+ ) {
// layout routes, i.e `/foo/route.tsx` or `/foo/_layout/route.tsx`
fsRouteType = 'layout'
- } else if (routePath.endsWith('/lazy')) {
+ } else if (routePath.endsWith('/lazy') && !isSuffixEscaped('lazy')) {
// lazy routes, i.e. `/foo.lazy.tsx`
fsRouteType = 'lazy'
- } else if (routePath.endsWith('/loader')) {
+ } else if (routePath.endsWith('/loader') && !isSuffixEscaped('loader')) {
// loader routes, i.e. `/foo.loader.tsx`
fsRouteType = 'loader'
- } else if (routePath.endsWith('/component')) {
+ } else if (
+ routePath.endsWith('/component') &&
+ !isSuffixEscaped('component')
+ ) {
// component routes, i.e. `/foo.component.tsx`
fsRouteType = 'component'
- } else if (routePath.endsWith('/pendingComponent')) {
+ } else if (
+ routePath.endsWith('/pendingComponent') &&
+ !isSuffixEscaped('pendingComponent')
+ ) {
// pending component routes, i.e. `/foo.pendingComponent.tsx`
fsRouteType = 'pendingComponent'
- } else if (routePath.endsWith('/errorComponent')) {
+ } else if (
+ routePath.endsWith('/errorComponent') &&
+ !isSuffixEscaped('errorComponent')
+ ) {
// error component routes, i.e. `/foo.errorComponent.tsx`
fsRouteType = 'errorComponent'
- } else if (routePath.endsWith('/notFoundComponent')) {
+ } else if (
+ routePath.endsWith('/notFoundComponent') &&
+ !isSuffixEscaped('notFoundComponent')
+ ) {
// not found component routes, i.e. `/foo.notFoundComponent.tsx`
fsRouteType = 'notFoundComponent'
}
@@ -323,11 +385,14 @@ export function getRouteMeta(
/**
* Used to validate if a route is a pathless layout route
* @param normalizedRoutePath Normalized route path, i.e `/foo/_layout/route.tsx` and `/foo._layout.route.tsx` to `/foo/_layout/route`
+ * @param originalRoutePath Original route path with brackets for escaped content
+ * @param routeType The route type determined from file extension
* @param config The `router-generator` configuration object
* @returns Boolean indicating if the route is a pathless layout route
*/
function isValidPathlessLayoutRoute(
normalizedRoutePath: string,
+ originalRoutePath: string,
routeType: FsRouteType,
config: Pick,
): boolean {
@@ -336,13 +401,18 @@ function isValidPathlessLayoutRoute(
}
const segments = normalizedRoutePath.split('/').filter(Boolean)
+ const originalSegments = originalRoutePath.split('/').filter(Boolean)
if (segments.length === 0) {
return false
}
const lastRouteSegment = segments[segments.length - 1]!
+ const lastOriginalSegment =
+ originalSegments[originalSegments.length - 1] || ''
const secondToLastRouteSegment = segments[segments.length - 2]
+ const secondToLastOriginalSegment =
+ originalSegments[originalSegments.length - 2]
// If segment === __root, then exit as false
if (lastRouteSegment === rootPathId) {
@@ -352,14 +422,25 @@ function isValidPathlessLayoutRoute(
// If segment === config.routeToken and secondToLastSegment is a string that starts with _, then exit as true
// Since the route is actually a configuration route for a layout/pathless route
// i.e. /foo/_layout/route.tsx === /foo/_layout.tsx
+ // But if the underscore is escaped, it's not a pathless layout
if (
lastRouteSegment === config.routeToken &&
- typeof secondToLastRouteSegment === 'string'
+ typeof secondToLastRouteSegment === 'string' &&
+ typeof secondToLastOriginalSegment === 'string'
) {
+ // Check if the underscore is escaped
+ if (hasEscapedLeadingUnderscore(secondToLastOriginalSegment)) {
+ return false
+ }
return secondToLastRouteSegment.startsWith('_')
}
- // Segment starts with _
+ // Segment starts with _ but check if it's escaped
+ // If the original segment has [_] at the start, the underscore is escaped and it's not a pathless layout
+ if (hasEscapedLeadingUnderscore(lastOriginalSegment)) {
+ return false
+ }
+
return (
lastRouteSegment !== config.indexToken &&
lastRouteSegment !== config.routeToken &&
diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts
index 6df57f9f7ab..3ec4b46576a 100644
--- a/packages/router-generator/src/generator.ts
+++ b/packages/router-generator/src/generator.ts
@@ -28,14 +28,15 @@ import {
getResolvedRouteNodeVariableName,
hasParentRoute,
isRouteNodeValidForAugmentation,
+ isSegmentPathless,
mergeImportDeclarations,
multiSortBy,
removeExt,
removeGroups,
removeLastSegmentFromPath,
- removeLayoutSegments,
+ removeLayoutSegmentsWithEscape,
removeTrailingSlash,
- removeUnderscores,
+ removeUnderscoresWithEscape,
replaceBackslash,
trimPathLeft,
} from './utils'
@@ -1363,16 +1364,30 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
node.path = determineNodePath(node)
const trimmedPath = trimPathLeft(node.path ?? '')
+ const trimmedOriginalPath = trimPathLeft(
+ node.originalRoutePath?.replace(
+ node.parent?.originalRoutePath ?? '',
+ '',
+ ) ?? '',
+ )
const split = trimmedPath.split('/')
+ const originalSplit = trimmedOriginalPath.split('/')
const lastRouteSegment = split[split.length - 1] ?? trimmedPath
+ const lastOriginalSegment =
+ originalSplit[originalSplit.length - 1] ?? trimmedOriginalPath
+ // A segment is non-path if it starts with underscore AND the underscore is not escaped
node.isNonPath =
- lastRouteSegment.startsWith('_') ||
+ isSegmentPathless(lastRouteSegment, lastOriginalSegment) ||
split.every((part) => this.routeGroupPatternRegex.test(part))
+ // Use escape-aware functions to compute cleanedPath
node.cleanedPath = removeGroups(
- removeUnderscores(removeLayoutSegments(node.path)) ?? '',
+ removeUnderscoresWithEscape(
+ removeLayoutSegmentsWithEscape(node.path, node.originalRoutePath),
+ node.originalRoutePath,
+ ),
)
if (
@@ -1431,6 +1446,8 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
if (!node.isVirtual && isPathlessLayoutWithPath) {
const immediateParentPath =
removeLastSegmentFromPath(node.routePath) || '/'
+ const immediateParentOriginalPath =
+ removeLastSegmentFromPath(node.originalRoutePath) || '/'
let searchPath = immediateParentPath
// Find nearest real (non-virtual, non-index) parent
@@ -1442,8 +1459,19 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
node.routePath?.replace(candidate.routePath ?? '', '') || '/'
const pathRelativeToParent =
immediateParentPath.replace(candidate.routePath ?? '', '') || '/'
+ const originalPathRelativeToParent =
+ immediateParentOriginalPath.replace(
+ candidate.originalRoutePath ?? '',
+ '',
+ ) || '/'
node.cleanedPath = removeGroups(
- removeUnderscores(removeLayoutSegments(pathRelativeToParent)) ?? '',
+ removeUnderscoresWithEscape(
+ removeLayoutSegmentsWithEscape(
+ pathRelativeToParent,
+ originalPathRelativeToParent,
+ ),
+ originalPathRelativeToParent,
+ ),
)
break
}
diff --git a/packages/router-generator/src/utils.ts b/packages/router-generator/src/utils.ts
index 9d9e58286bd..9fa8d229a43 100644
--- a/packages/router-generator/src/utils.ts
+++ b/packages/router-generator/src/utils.ts
@@ -140,23 +140,26 @@ export function removeTrailingSlash(s: string) {
const BRACKET_CONTENT_RE = /\[(.*?)\]/g
const SPLIT_REGEX = /(?',
- '|',
- '!',
- '$',
- '%',
- '_',
- ])
+/**
+ * Characters that cannot be escaped in square brackets.
+ * These are characters that would cause issues in URLs or file systems.
+ */
+const DISALLOWED_ESCAPE_CHARS = new Set([
+ '/',
+ '\\',
+ '?',
+ '#',
+ ':',
+ '*',
+ '<',
+ '>',
+ '|',
+ '!',
+ '$',
+ '%',
+])
+export function determineInitialRoutePath(routePath: string) {
const originalRoutePath =
cleanPath(
`/${(cleanPath(routePath) || '').split(SPLIT_REGEX).join('/')}`,
@@ -201,6 +204,47 @@ export function determineInitialRoutePath(routePath: string) {
}
}
+/**
+ * Checks if a segment is fully escaped (entirely wrapped in brackets with no nested brackets).
+ * E.g., "[index]" -> true, "[_layout]" -> true, "foo[.]bar" -> false, "index" -> false
+ */
+function isFullyEscapedSegment(originalSegment: string): boolean {
+ return (
+ originalSegment.startsWith('[') &&
+ originalSegment.endsWith(']') &&
+ !originalSegment.slice(1, -1).includes('[') &&
+ !originalSegment.slice(1, -1).includes(']')
+ )
+}
+
+/**
+ * Checks if the leading underscore in a segment is escaped.
+ * Returns true if:
+ * - Segment starts with [_] pattern: "[_]layout" -> "_layout"
+ * - Segment is fully escaped and content starts with _: "[_1nd3x]" -> "_1nd3x"
+ */
+export function hasEscapedLeadingUnderscore(originalSegment: string): boolean {
+ // Pattern: [_]something or [_something]
+ return (
+ originalSegment.startsWith('[_]') ||
+ (originalSegment.startsWith('[_') && isFullyEscapedSegment(originalSegment))
+ )
+}
+
+/**
+ * Checks if the trailing underscore in a segment is escaped.
+ * Returns true if:
+ * - Segment ends with [_] pattern: "blog[_]" -> "blog_"
+ * - Segment is fully escaped and content ends with _: "[_r0ut3_]" -> "_r0ut3_"
+ */
+export function hasEscapedTrailingUnderscore(originalSegment: string): boolean {
+ // Pattern: something[_] or [something_]
+ return (
+ originalSegment.endsWith('[_]') ||
+ (originalSegment.endsWith('_]') && isFullyEscapedSegment(originalSegment))
+ )
+}
+
const backslashRegex = /\\/g
export function replaceBackslash(s: string) {
@@ -271,6 +315,92 @@ export function removeUnderscores(s?: string) {
.replace(underscoreSlashRegex, '/')
}
+/**
+ * Removes underscores from a path, but preserves underscores that were escaped
+ * in the original path (indicated by [_] syntax).
+ *
+ * @param routePath - The path with brackets removed
+ * @param originalPath - The original path that may contain [_] escape sequences
+ * @returns The path with non-escaped underscores removed
+ */
+export function removeUnderscoresWithEscape(
+ routePath?: string,
+ originalPath?: string,
+): string {
+ if (!routePath) return ''
+ if (!originalPath) return removeUnderscores(routePath) ?? ''
+
+ const routeSegments = routePath.split('/')
+ const originalSegments = originalPath.split('/')
+
+ const newSegments = routeSegments.map((segment, i) => {
+ const originalSegment = originalSegments[i] || ''
+
+ // Check if leading underscore is escaped
+ const leadingEscaped = hasEscapedLeadingUnderscore(originalSegment)
+ // Check if trailing underscore is escaped
+ const trailingEscaped = hasEscapedTrailingUnderscore(originalSegment)
+
+ let result = segment
+
+ // Remove leading underscore only if not escaped
+ if (result.startsWith('_') && !leadingEscaped) {
+ result = result.slice(1)
+ }
+
+ // Remove trailing underscore only if not escaped
+ if (result.endsWith('_') && !trailingEscaped) {
+ result = result.slice(0, -1)
+ }
+
+ return result
+ })
+
+ return newSegments.join('/')
+}
+
+/**
+ * Removes layout segments (segments starting with underscore) from a path,
+ * but preserves segments where the underscore was escaped.
+ *
+ * @param routePath - The path with brackets removed
+ * @param originalPath - The original path that may contain [_] escape sequences
+ * @returns The path with non-escaped layout segments removed
+ */
+export function removeLayoutSegmentsWithEscape(
+ routePath: string = '/',
+ originalPath?: string,
+): string {
+ if (!originalPath) return removeLayoutSegments(routePath)
+
+ const routeSegments = routePath.split('/')
+ const originalSegments = originalPath.split('/')
+
+ // Keep segments that are NOT pathless (i.e., don't start with unescaped underscore)
+ const newSegments = routeSegments.filter((segment, i) => {
+ const originalSegment = originalSegments[i] || ''
+ return !isSegmentPathless(segment, originalSegment)
+ })
+
+ return newSegments.join('/')
+}
+
+/**
+ * Checks if a segment should be treated as a pathless/layout segment.
+ * A segment is pathless if it starts with underscore and the underscore is not escaped.
+ *
+ * @param segment - The segment from routePath (brackets removed)
+ * @param originalSegment - The segment from originalRoutePath (may contain brackets)
+ * @returns true if the segment is pathless (has non-escaped leading underscore)
+ */
+export function isSegmentPathless(
+ segment: string,
+ originalSegment: string,
+): boolean {
+ if (!segment.startsWith('_')) return false
+ return !hasEscapedLeadingUnderscore(originalSegment)
+}
+
function escapeRegExp(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
@@ -495,7 +625,13 @@ export const inferPath = (routeNode: RouteNode): string => {
*/
export const inferFullPath = (routeNode: RouteNode): string => {
const fullPath = removeGroups(
- removeUnderscores(removeLayoutSegments(routeNode.routePath)) ?? '',
+ removeUnderscoresWithEscape(
+ removeLayoutSegmentsWithEscape(
+ routeNode.routePath,
+ routeNode.originalRoutePath,
+ ),
+ routeNode.originalRoutePath,
+ ),
)
return routeNode.cleanedPath === '/' ? fullPath : fullPath.replace(/\/$/, '')
diff --git a/packages/router-generator/tests/generator.test.ts b/packages/router-generator/tests/generator.test.ts
index 8b737e0387a..13aa39db3a7 100644
--- a/packages/router-generator/tests/generator.test.ts
+++ b/packages/router-generator/tests/generator.test.ts
@@ -75,6 +75,10 @@ function rewriteConfigByFolderName(folderName: string, config: Config) {
config.indexToken = '_1nd3x'
config.routeToken = '_r0ut3_'
break
+ case 'escaped-custom-tokens':
+ config.indexToken = '_1nd3x'
+ config.routeToken = '_r0ut3_'
+ break
case 'virtual':
{
const virtualRouteConfig = rootRoute('root.tsx', [
diff --git a/packages/router-generator/tests/generator/escaped-custom-tokens/routeTree.snapshot.ts b/packages/router-generator/tests/generator/escaped-custom-tokens/routeTree.snapshot.ts
new file mode 100644
index 00000000000..a555744dff6
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-custom-tokens/routeTree.snapshot.ts
@@ -0,0 +1,137 @@
+/* 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 R1nd3xRouteImport } from './routes/[_1nd3x]'
+import { Route as BlogR0ut3RouteImport } from './routes/blog._r0ut3_'
+import { Route as R1nd3xRouteImport } from './routes/_1nd3x'
+import { Route as Nested1nd3xRouteImport } from './routes/nested.[_1nd3x]'
+import { Route as PostsR0ut3RouteImport } from './routes/posts.[_r0ut3_]'
+
+const R1nd3xRoute = R1nd3xRouteImport.update({
+ id: '/_1nd3x',
+ path: '/_1nd3x',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const BlogR0ut3Route = BlogR0ut3RouteImport.update({
+ id: '/blog',
+ path: '/blog',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const R1nd3xRoute = R1nd3xRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const Nested1nd3xRoute = Nested1nd3xRouteImport.update({
+ id: '/nested/_1nd3x',
+ path: '/nested/_1nd3x',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const PostsR0ut3Route = PostsR0ut3RouteImport.update({
+ id: '/posts/_r0ut3_',
+ path: '/posts/_r0ut3_',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof R1nd3xRoute
+ '/blog': typeof BlogR0ut3Route
+ '/_1nd3x': typeof R1nd3xRoute
+ '/nested/_1nd3x': typeof Nested1nd3xRoute
+ '/posts/_r0ut3_': typeof PostsR0ut3Route
+}
+export interface FileRoutesByTo {
+ '/': typeof R1nd3xRoute
+ '/blog': typeof BlogR0ut3Route
+ '/_1nd3x': typeof R1nd3xRoute
+ '/nested/_1nd3x': typeof Nested1nd3xRoute
+ '/posts/_r0ut3_': typeof PostsR0ut3Route
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof R1nd3xRoute
+ '/blog': typeof BlogR0ut3Route
+ '/_1nd3x': typeof R1nd3xRoute
+ '/nested/_1nd3x': typeof Nested1nd3xRoute
+ '/posts/_r0ut3_': typeof PostsR0ut3Route
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/blog' | '/_1nd3x' | '/nested/_1nd3x' | '/posts/_r0ut3_'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/blog' | '/_1nd3x' | '/nested/_1nd3x' | '/posts/_r0ut3_'
+ id:
+ | '__root__'
+ | '/'
+ | '/blog'
+ | '/_1nd3x'
+ | '/nested/_1nd3x'
+ | '/posts/_r0ut3_'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ R1nd3xRoute: typeof R1nd3xRoute
+ BlogR0ut3Route: typeof BlogR0ut3Route
+ R1nd3xRoute: typeof R1nd3xRoute
+ Nested1nd3xRoute: typeof Nested1nd3xRoute
+ PostsR0ut3Route: typeof PostsR0ut3Route
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/_1nd3x': {
+ id: '/_1nd3x'
+ path: '/_1nd3x'
+ fullPath: '/_1nd3x'
+ preLoaderRoute: typeof R1nd3xRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/blog': {
+ id: '/blog'
+ path: '/blog'
+ fullPath: '/blog'
+ preLoaderRoute: typeof BlogR0ut3RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof R1nd3xRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/nested/_1nd3x': {
+ id: '/nested/_1nd3x'
+ path: '/nested/_1nd3x'
+ fullPath: '/nested/_1nd3x'
+ preLoaderRoute: typeof Nested1nd3xRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/posts/_r0ut3_': {
+ id: '/posts/_r0ut3_'
+ path: '/posts/_r0ut3_'
+ fullPath: '/posts/_r0ut3_'
+ preLoaderRoute: typeof PostsR0ut3RouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ R1nd3xRoute: R1nd3xRoute,
+ BlogR0ut3Route: BlogR0ut3Route,
+ R1nd3xRoute: R1nd3xRoute,
+ Nested1nd3xRoute: Nested1nd3xRoute,
+ PostsR0ut3Route: PostsR0ut3Route,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/packages/router-generator/tests/generator/escaped-custom-tokens/routes/[_1nd3x].tsx b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/[_1nd3x].tsx
new file mode 100644
index 00000000000..cf5498ee1f0
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/[_1nd3x].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// Escaped custom indexToken - should be literal /_1nd3x path, NOT treated as index route
+export const Route = createFileRoute('/_1nd3x')()
diff --git a/packages/router-generator/tests/generator/escaped-custom-tokens/routes/_1nd3x.tsx b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/_1nd3x.tsx
new file mode 100644
index 00000000000..e1e2b51af6d
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/_1nd3x.tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// Normal index route using custom indexToken
+export const Route = createFileRoute('/')()
diff --git a/packages/router-generator/tests/generator/escaped-custom-tokens/routes/__root.tsx b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/__root.tsx
new file mode 100644
index 00000000000..c11ed1e011b
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/__root.tsx
@@ -0,0 +1,2 @@
+// @ts-nocheck
+export const Route = createRootRoute()
diff --git a/packages/router-generator/tests/generator/escaped-custom-tokens/routes/blog._r0ut3_.tsx b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/blog._r0ut3_.tsx
new file mode 100644
index 00000000000..849c7e619cf
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/blog._r0ut3_.tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// Normal layout config using custom routeToken
+export const Route = createFileRoute('/blog')()
diff --git a/packages/router-generator/tests/generator/escaped-custom-tokens/routes/nested.[_1nd3x].tsx b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/nested.[_1nd3x].tsx
new file mode 100644
index 00000000000..bf2a046b062
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/nested.[_1nd3x].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// Escaped custom indexToken in nested path - should be literal /nested/_1nd3x, NOT index of /nested
+export const Route = createFileRoute('/nested/_1nd3x')()
diff --git a/packages/router-generator/tests/generator/escaped-custom-tokens/routes/posts.[_r0ut3_].tsx b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/posts.[_r0ut3_].tsx
new file mode 100644
index 00000000000..2b0382cce80
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-custom-tokens/routes/posts.[_r0ut3_].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// Escaped custom routeToken - should be literal /posts/_r0ut3_ path, NOT treated as layout config
+export const Route = createFileRoute('/posts/_r0ut3_')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routeTree.snapshot.ts b/packages/router-generator/tests/generator/escaped-special-strings/routeTree.snapshot.ts
new file mode 100644
index 00000000000..ac3c416e478
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routeTree.snapshot.ts
@@ -0,0 +1,273 @@
+/* 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 RouteRouteImport } from './routes/[route]'
+import { Route as LazyRouteImport } from './routes/[lazy]'
+import { Route as Foo_barRouteImport } from './routes/foo[_]bar'
+import { Route as BlogRouteImport } from './routes/blog[_]'
+import { Route as Api_v2_usersRouteImport } from './routes/api[_]v2[_]users'
+import { Route as Prefix_middle_suffixRouteImport } from './routes/[_]prefix[_]middle[_]suffix'
+import { Route as LayoutRouteImport } from './routes/[_]layout'
+import { Route as AuthRouteRouteImport } from './routes/[_]auth.route'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as NestedIndexRouteImport } from './routes/nested.[index]'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/index',
+ path: '/index',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RouteRoute = RouteRouteImport.update({
+ id: '/route',
+ path: '/route',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LazyRoute = LazyRouteImport.update({
+ id: '/lazy',
+ path: '/lazy',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const Foo_barRoute = Foo_barRouteImport.update({
+ id: '/foo_bar',
+ path: '/foo_bar',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const BlogRoute = BlogRouteImport.update({
+ id: '/blog_',
+ path: '/blog_',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const Api_v2_usersRoute = Api_v2_usersRouteImport.update({
+ id: '/api_v2_users',
+ path: '/api_v2_users',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const Prefix_middle_suffixRoute = Prefix_middle_suffixRouteImport.update({
+ id: '/_prefix_middle_suffix',
+ path: '/_prefix_middle_suffix',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LayoutRoute = LayoutRouteImport.update({
+ id: '/_layout',
+ path: '/_layout',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const AuthRouteRoute = AuthRouteRouteImport.update({
+ id: '/_auth',
+ path: '/_auth',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const NestedIndexRoute = NestedIndexRouteImport.update({
+ id: '/nested/index',
+ path: '/nested/index',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/_auth': typeof AuthRouteRoute
+ '/_layout': typeof LayoutRoute
+ '/_prefix_middle_suffix': typeof Prefix_middle_suffixRoute
+ '/api_v2_users': typeof Api_v2_usersRoute
+ '/blog_': typeof BlogRoute
+ '/foo_bar': typeof Foo_barRoute
+ '/index': typeof IndexRoute
+ '/lazy': typeof LazyRoute
+ '/route': typeof RouteRoute
+ '/nested/index': typeof NestedIndexRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/_auth': typeof AuthRouteRoute
+ '/_layout': typeof LayoutRoute
+ '/_prefix_middle_suffix': typeof Prefix_middle_suffixRoute
+ '/api_v2_users': typeof Api_v2_usersRoute
+ '/blog_': typeof BlogRoute
+ '/foo_bar': typeof Foo_barRoute
+ '/index': typeof IndexRoute
+ '/lazy': typeof LazyRoute
+ '/route': typeof RouteRoute
+ '/nested/index': typeof NestedIndexRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/_auth': typeof AuthRouteRoute
+ '/_layout': typeof LayoutRoute
+ '/_prefix_middle_suffix': typeof Prefix_middle_suffixRoute
+ '/api_v2_users': typeof Api_v2_usersRoute
+ '/blog_': typeof BlogRoute
+ '/foo_bar': typeof Foo_barRoute
+ '/index': typeof IndexRoute
+ '/lazy': typeof LazyRoute
+ '/route': typeof RouteRoute
+ '/nested/index': typeof NestedIndexRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths:
+ | '/'
+ | '/_auth'
+ | '/_layout'
+ | '/_prefix_middle_suffix'
+ | '/api_v2_users'
+ | '/blog_'
+ | '/foo_bar'
+ | '/index'
+ | '/lazy'
+ | '/route'
+ | '/nested/index'
+ fileRoutesByTo: FileRoutesByTo
+ to:
+ | '/'
+ | '/_auth'
+ | '/_layout'
+ | '/_prefix_middle_suffix'
+ | '/api_v2_users'
+ | '/blog_'
+ | '/foo_bar'
+ | '/index'
+ | '/lazy'
+ | '/route'
+ | '/nested/index'
+ id:
+ | '__root__'
+ | '/'
+ | '/_auth'
+ | '/_layout'
+ | '/_prefix_middle_suffix'
+ | '/api_v2_users'
+ | '/blog_'
+ | '/foo_bar'
+ | '/index'
+ | '/lazy'
+ | '/route'
+ | '/nested/index'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ AuthRouteRoute: typeof AuthRouteRoute
+ LayoutRoute: typeof LayoutRoute
+ Prefix_middle_suffixRoute: typeof Prefix_middle_suffixRoute
+ Api_v2_usersRoute: typeof Api_v2_usersRoute
+ BlogRoute: typeof BlogRoute
+ Foo_barRoute: typeof Foo_barRoute
+ IndexRoute: typeof IndexRoute
+ LazyRoute: typeof LazyRoute
+ RouteRoute: typeof RouteRoute
+ NestedIndexRoute: typeof NestedIndexRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/index': {
+ id: '/index'
+ path: '/index'
+ fullPath: '/index'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/route': {
+ id: '/route'
+ path: '/route'
+ fullPath: '/route'
+ preLoaderRoute: typeof RouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/lazy': {
+ id: '/lazy'
+ path: '/lazy'
+ fullPath: '/lazy'
+ preLoaderRoute: typeof LazyRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/foo_bar': {
+ id: '/foo_bar'
+ path: '/foo_bar'
+ fullPath: '/foo_bar'
+ preLoaderRoute: typeof Foo_barRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/blog_': {
+ id: '/blog_'
+ path: '/blog_'
+ fullPath: '/blog_'
+ preLoaderRoute: typeof BlogRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/api_v2_users': {
+ id: '/api_v2_users'
+ path: '/api_v2_users'
+ fullPath: '/api_v2_users'
+ preLoaderRoute: typeof Api_v2_usersRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_prefix_middle_suffix': {
+ id: '/_prefix_middle_suffix'
+ path: '/_prefix_middle_suffix'
+ fullPath: '/_prefix_middle_suffix'
+ preLoaderRoute: typeof Prefix_middle_suffixRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_layout': {
+ id: '/_layout'
+ path: '/_layout'
+ fullPath: '/_layout'
+ preLoaderRoute: typeof LayoutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_auth': {
+ id: '/_auth'
+ path: '/_auth'
+ fullPath: '/_auth'
+ preLoaderRoute: typeof AuthRouteRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/nested/index': {
+ id: '/nested/index'
+ path: '/nested/index'
+ fullPath: '/nested/index'
+ preLoaderRoute: typeof NestedIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ AuthRouteRoute: AuthRouteRoute,
+ LayoutRoute: LayoutRoute,
+ Prefix_middle_suffixRoute: Prefix_middle_suffixRoute,
+ Api_v2_usersRoute: Api_v2_usersRoute,
+ BlogRoute: BlogRoute,
+ Foo_barRoute: Foo_barRoute,
+ IndexRoute: IndexRoute,
+ LazyRoute: LazyRoute,
+ RouteRoute: RouteRoute,
+ NestedIndexRoute: NestedIndexRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]auth.route.tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]auth.route.tsx
new file mode 100644
index 00000000000..8451a485f72
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]auth.route.tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a layout config for /_auth path (underscore is escaped, but .route suffix is honored)
+export const Route = createFileRoute('/_auth')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]layout.tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]layout.tsx
new file mode 100644
index 00000000000..d20b05c5540
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]layout.tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /_layout path, NOT treated as a pathless layout
+export const Route = createFileRoute('/_layout')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]prefix[_]middle[_]suffix.tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]prefix[_]middle[_]suffix.tsx
new file mode 100644
index 00000000000..65d7e2236b5
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/[_]prefix[_]middle[_]suffix.tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /_prefix_middle_suffix path with underscores everywhere
+export const Route = createFileRoute('/_prefix_middle_suffix')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/[index].tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/[index].tsx
new file mode 100644
index 00000000000..422cd911732
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/[index].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /index path, NOT treated as the index route
+export const Route = createFileRoute('/index')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/[lazy].tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/[lazy].tsx
new file mode 100644
index 00000000000..0720796d510
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/[lazy].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /lazy path, NOT treated as a lazy-loaded route
+export const Route = createFileRoute('/lazy')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/[route].tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/[route].tsx
new file mode 100644
index 00000000000..e8d861dfedc
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/[route].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /route path, NOT treated as a layout config file
+export const Route = createFileRoute('/route')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/__root.tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/__root.tsx
new file mode 100644
index 00000000000..c11ed1e011b
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/__root.tsx
@@ -0,0 +1,2 @@
+// @ts-nocheck
+export const Route = createRootRoute()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/api[_]v2[_]users.tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/api[_]v2[_]users.tsx
new file mode 100644
index 00000000000..2a6cfceef7e
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/api[_]v2[_]users.tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /api_v2_users path with multiple underscores in middle
+export const Route = createFileRoute('/api_v2_users')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/blog[_].tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/blog[_].tsx
new file mode 100644
index 00000000000..0e03cea09e0
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/blog[_].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /blog_ path, NOT escaping from parent layout
+export const Route = createFileRoute('/blog_')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/foo[_]bar.tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/foo[_]bar.tsx
new file mode 100644
index 00000000000..1b3ec54a03c
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/foo[_]bar.tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be a literal /foo_bar path with underscore in middle
+export const Route = createFileRoute('/foo_bar')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/index.tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/index.tsx
new file mode 100644
index 00000000000..f338b4f8e93
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/index.tsx
@@ -0,0 +1,3 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+export const Route = createFileRoute('/')()
diff --git a/packages/router-generator/tests/generator/escaped-special-strings/routes/nested.[index].tsx b/packages/router-generator/tests/generator/escaped-special-strings/routes/nested.[index].tsx
new file mode 100644
index 00000000000..33b5b3e3886
--- /dev/null
+++ b/packages/router-generator/tests/generator/escaped-special-strings/routes/nested.[index].tsx
@@ -0,0 +1,4 @@
+import { createFileRoute } from '@tanstack/react-router'
+// @ts-nocheck
+// This should be /nested/index (literal), NOT treated as the index of /nested
+export const Route = createFileRoute('/nested/index')()
diff --git a/packages/router-generator/tests/utils.test.ts b/packages/router-generator/tests/utils.test.ts
index c556edeaf55..8d1d1be3723 100644
--- a/packages/router-generator/tests/utils.test.ts
+++ b/packages/router-generator/tests/utils.test.ts
@@ -3,12 +3,17 @@ import {
RoutePrefixMap,
cleanPath,
determineInitialRoutePath,
+ hasEscapedLeadingUnderscore,
+ hasEscapedTrailingUnderscore,
+ isSegmentPathless,
mergeImportDeclarations,
multiSortBy,
removeExt,
+ removeLayoutSegmentsWithEscape,
removeLeadingUnderscores,
removeTrailingUnderscores,
removeUnderscores,
+ removeUnderscoresWithEscape,
routePathToVariable,
} from '../src/utils'
import type { ImportDeclaration, RouteNode } from '../src/types'
@@ -76,7 +81,7 @@ describe('determineInitialRoutePath', () => {
expect(consoleSpy).toBeCalledWith(
'Error: Disallowed character "/" found in square brackets in route path "/a[/]".\n' +
- 'You cannot use any of the following characters in square brackets: /, \\, ?, #, :, *, <, >, |, !, $, %, _\n' +
+ 'You cannot use any of the following characters in square brackets: /, \\, ?, #, :, *, <, >, |, !, $, %\n' +
'Please remove and/or replace them.',
)
@@ -296,6 +301,199 @@ describe('removeTrailingUnderscores', () => {
})
})
+describe('hasEscapedLeadingUnderscore', () => {
+ it('returns true for [_] prefix pattern', () => {
+ expect(hasEscapedLeadingUnderscore('[_]layout')).toBe(true)
+ expect(hasEscapedLeadingUnderscore('[_]foo')).toBe(true)
+ expect(hasEscapedLeadingUnderscore('[_]')).toBe(true)
+ })
+
+ it('returns true for fully escaped segment starting with underscore', () => {
+ expect(hasEscapedLeadingUnderscore('[_layout]')).toBe(true)
+ expect(hasEscapedLeadingUnderscore('[_foo]')).toBe(true)
+ expect(hasEscapedLeadingUnderscore('[_1nd3x]')).toBe(true)
+ expect(hasEscapedLeadingUnderscore('[_]')).toBe(true)
+ })
+
+ it('returns false for non-escaped leading underscore', () => {
+ expect(hasEscapedLeadingUnderscore('_layout')).toBe(false)
+ expect(hasEscapedLeadingUnderscore('_foo')).toBe(false)
+ })
+
+ it('returns false for segments without leading underscore', () => {
+ expect(hasEscapedLeadingUnderscore('layout')).toBe(false)
+ expect(hasEscapedLeadingUnderscore('[layout]')).toBe(false)
+ expect(hasEscapedLeadingUnderscore('foo[_]')).toBe(false)
+ })
+
+ it('returns false for partial escapes with nested brackets', () => {
+ expect(hasEscapedLeadingUnderscore('[_foo[bar]')).toBe(false)
+ expect(hasEscapedLeadingUnderscore('[_foo]bar]')).toBe(false)
+ })
+})
+
+describe('hasEscapedTrailingUnderscore', () => {
+ it('returns true for [_] suffix pattern', () => {
+ expect(hasEscapedTrailingUnderscore('blog[_]')).toBe(true)
+ expect(hasEscapedTrailingUnderscore('foo[_]')).toBe(true)
+ expect(hasEscapedTrailingUnderscore('[_]')).toBe(true)
+ })
+
+ it('returns true for fully escaped segment ending with underscore', () => {
+ expect(hasEscapedTrailingUnderscore('[blog_]')).toBe(true)
+ expect(hasEscapedTrailingUnderscore('[foo_]')).toBe(true)
+ expect(hasEscapedTrailingUnderscore('[_r0ut3_]')).toBe(true)
+ expect(hasEscapedTrailingUnderscore('[_]')).toBe(true)
+ })
+
+ it('returns false for non-escaped trailing underscore', () => {
+ expect(hasEscapedTrailingUnderscore('blog_')).toBe(false)
+ expect(hasEscapedTrailingUnderscore('foo_')).toBe(false)
+ })
+
+ it('returns false for segments without trailing underscore', () => {
+ expect(hasEscapedTrailingUnderscore('blog')).toBe(false)
+ expect(hasEscapedTrailingUnderscore('[blog]')).toBe(false)
+ expect(hasEscapedTrailingUnderscore('[_]foo')).toBe(false)
+ })
+
+ it('returns false for partial escapes with nested brackets', () => {
+ expect(hasEscapedTrailingUnderscore('[foo[bar]_]')).toBe(false)
+ expect(hasEscapedTrailingUnderscore('[foo]bar_]')).toBe(false)
+ })
+})
+
+describe('isSegmentPathless', () => {
+ it('returns true for non-escaped leading underscore', () => {
+ expect(isSegmentPathless('_layout', '_layout')).toBe(true)
+ expect(isSegmentPathless('_foo', '_foo')).toBe(true)
+ })
+
+ it('returns false for escaped leading underscore with [_] prefix', () => {
+ expect(isSegmentPathless('_layout', '[_]layout')).toBe(false)
+ expect(isSegmentPathless('_foo', '[_]foo')).toBe(false)
+ })
+
+ it('returns false for fully escaped segment', () => {
+ expect(isSegmentPathless('_layout', '[_layout]')).toBe(false)
+ expect(isSegmentPathless('_1nd3x', '[_1nd3x]')).toBe(false)
+ })
+
+ it('returns false for segments not starting with underscore', () => {
+ expect(isSegmentPathless('layout', 'layout')).toBe(false)
+ expect(isSegmentPathless('foo', '[foo]')).toBe(false)
+ })
+})
+
+describe('removeUnderscoresWithEscape', () => {
+ it('removes non-escaped leading underscores', () => {
+ expect(removeUnderscoresWithEscape('/_layout', '/_layout')).toBe('/layout')
+ expect(removeUnderscoresWithEscape('/_foo/_bar', '/_foo/_bar')).toBe(
+ '/foo/bar',
+ )
+ })
+
+ it('removes non-escaped trailing underscores', () => {
+ expect(removeUnderscoresWithEscape('/blog_', '/blog_')).toBe('/blog')
+ expect(removeUnderscoresWithEscape('/foo_/bar_', '/foo_/bar_')).toBe(
+ '/foo/bar',
+ )
+ })
+
+ it('preserves escaped leading underscores with [_] prefix', () => {
+ expect(removeUnderscoresWithEscape('/_layout', '/[_]layout')).toBe(
+ '/_layout',
+ )
+ expect(removeUnderscoresWithEscape('/_foo', '/[_]foo')).toBe('/_foo')
+ })
+
+ it('preserves escaped trailing underscores with [_] suffix', () => {
+ expect(removeUnderscoresWithEscape('/blog_', '/blog[_]')).toBe('/blog_')
+ expect(removeUnderscoresWithEscape('/foo_', '/foo[_]')).toBe('/foo_')
+ })
+
+ it('preserves fully escaped segments with underscores', () => {
+ expect(removeUnderscoresWithEscape('/_layout', '/[_layout]')).toBe(
+ '/_layout',
+ )
+ expect(removeUnderscoresWithEscape('/_r0ut3_', '/[_r0ut3_]')).toBe(
+ '/_r0ut3_',
+ )
+ })
+
+ it('handles mixed escaped and non-escaped underscores', () => {
+ expect(
+ removeUnderscoresWithEscape('/_foo/_bar_/baz_', '/_foo/[_]bar_/baz[_]'),
+ ).toBe('/foo/_bar/baz_')
+ })
+
+ it('falls back to removeUnderscores when no originalPath', () => {
+ expect(removeUnderscoresWithEscape('/_foo_')).toBe('/foo')
+ expect(removeUnderscoresWithEscape('/_foo_', undefined)).toBe('/foo')
+ })
+
+ it('returns empty string for empty/undefined routePath', () => {
+ expect(removeUnderscoresWithEscape(undefined)).toBe('')
+ expect(removeUnderscoresWithEscape('')).toBe('')
+ })
+})
+
+describe('removeLayoutSegmentsWithEscape', () => {
+ it('removes non-escaped layout segments', () => {
+ expect(removeLayoutSegmentsWithEscape('/_layout/foo', '/_layout/foo')).toBe(
+ '/foo',
+ )
+ expect(
+ removeLayoutSegmentsWithEscape(
+ '/_auth/_admin/dashboard',
+ '/_auth/_admin/dashboard',
+ ),
+ ).toBe('/dashboard')
+ })
+
+ it('preserves escaped layout segments with [_] prefix', () => {
+ expect(
+ removeLayoutSegmentsWithEscape('/_layout/foo', '/[_]layout/foo'),
+ ).toBe('/_layout/foo')
+ expect(
+ removeLayoutSegmentsWithEscape('/_auth/dashboard', '/[_]auth/dashboard'),
+ ).toBe('/_auth/dashboard')
+ })
+
+ it('preserves fully escaped segments starting with underscore', () => {
+ expect(
+ removeLayoutSegmentsWithEscape('/_layout/foo', '/[_layout]/foo'),
+ ).toBe('/_layout/foo')
+ expect(removeLayoutSegmentsWithEscape('/_1nd3x/bar', '/[_1nd3x]/bar')).toBe(
+ '/_1nd3x/bar',
+ )
+ })
+
+ it('handles mixed escaped and non-escaped layout segments', () => {
+ expect(
+ removeLayoutSegmentsWithEscape(
+ '/_auth/_admin/dashboard',
+ '/[_]auth/_admin/dashboard',
+ ),
+ ).toBe('/_auth/dashboard')
+ expect(
+ removeLayoutSegmentsWithEscape('/_foo/_bar/_baz', '/_foo/[_bar]/_baz'),
+ ).toBe('/_bar')
+ })
+
+ it('falls back to removeLayoutSegments when no originalPath', () => {
+ expect(removeLayoutSegmentsWithEscape('/_foo/bar/_baz')).toBe('/bar')
+ expect(removeLayoutSegmentsWithEscape('/_foo/bar/_baz', undefined)).toBe(
+ '/bar',
+ )
+ })
+
+ it('handles root path', () => {
+ expect(removeLayoutSegmentsWithEscape('/')).toBe('/')
+ expect(removeLayoutSegmentsWithEscape()).toBe('/')
+ })
+})
+
describe('routePathToVariable', () => {
it.each([
['/test/$/index', 'TestSplatIndex'],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6d94b8fbe52..4a3eae65cb1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -670,6 +670,40 @@ importers:
specifier: ^7.1.7
version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ e2e/react-router/escaped-special-strings:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/router-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/router-plugin
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+
e2e/react-router/generator-cli-only:
dependencies:
'@tailwindcss/postcss':
@@ -10555,7 +10589,7 @@ importers:
devDependencies:
'@netlify/vite-plugin-tanstack-start':
specifier: ^1.1.4
- version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
'@tailwindcss/postcss':
specifier: ^4.1.15
version: 4.1.15
@@ -26798,13 +26832,13 @@ snapshots:
uuid: 11.1.0
write-file-atomic: 5.0.1
- '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)':
+ '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/config': 23.2.0
'@netlify/dev-utils': 4.3.0
'@netlify/edge-functions-dev': 1.0.0
- '@netlify/functions-dev': 1.0.0(rollup@4.52.5)
+ '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5)
'@netlify/headers': 2.1.0
'@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)
'@netlify/redirects': 3.1.0
@@ -26872,12 +26906,12 @@ snapshots:
dependencies:
'@netlify/types': 2.1.0
- '@netlify/functions-dev@1.0.0(rollup@4.52.5)':
+ '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/dev-utils': 4.3.0
'@netlify/functions': 5.0.0
- '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5)
+ '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1
@@ -26967,9 +27001,9 @@ snapshots:
'@netlify/types@2.1.0': {}
- '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+ '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
dependencies:
- '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
optionalDependencies:
'@tanstack/solid-start': link:packages/solid-start
@@ -26997,9 +27031,9 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+ '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
dependencies:
- '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)
+ '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)
'@netlify/dev-utils': 4.3.0
dedent: 1.7.0(babel-plugin-macros@3.1.0)
vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
@@ -27027,13 +27061,13 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)':
+ '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.4
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 2.7.1
- '@vercel/nft': 0.29.4(rollup@4.52.5)
+ '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5)
archiver: 7.0.1
common-path-prefix: 3.0.0
copy-file: 11.1.0
@@ -30101,7 +30135,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vercel/nft@0.29.4(rollup@4.52.5)':
+ '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13)
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)