diff --git a/docs/router/framework/react/routing/virtual-file-routes.md b/docs/router/framework/react/routing/virtual-file-routes.md
index 3e308ee7c9d..a285a49223a 100644
--- a/docs/router/framework/react/routing/virtual-file-routes.md
+++ b/docs/router/framework/react/routing/virtual-file-routes.md
@@ -224,6 +224,43 @@ export const routes = rootRoute('root.tsx', [
])
```
+### Merging Physical Routes at Current Level
+
+You can also use `physical` with an empty path prefix (or a single argument) to merge routes from a physical directory directly at the current level, without adding a path prefix. This is useful when you want to organize your routes into separate directories but have them appear at the same URL level.
+
+Consider the following file structure:
+
+```
+/routes
+├── __root.tsx
+├── about.tsx
+└── features
+ ├── index.tsx
+ └── contact.tsx
+```
+
+You can merge the `features` directory routes at the root level:
+
+```tsx
+// routes.ts
+import { physical, rootRoute, route } from '@tanstack/virtual-file-routes'
+
+export const routes = rootRoute('__root.tsx', [
+ route('/about', 'about.tsx'),
+ // Merge features/ routes at root level (no path prefix)
+ physical('features'),
+ // Or equivalently: physical('', 'features')
+])
+```
+
+This will produce the following routes:
+
+- `/about` - from `about.tsx`
+- `/` - from `features/index.tsx`
+- `/contact` - from `features/contact.tsx`
+
+> **Note:** When merging at the same level, ensure there are no conflicting route paths between your virtual routes and the physical directory routes. If a conflict occurs (e.g., both have an `/about` route), the generator will throw an error.
+
## Virtual Routes inside of TanStack Router File Based routing
The previous section showed you how you can use TanStack Router's File Based routing convention inside of a virtual route configuration.
diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts
index 0ecb325ceee..01f485581f8 100644
--- a/packages/router-generator/src/generator.ts
+++ b/packages/router-generator/src/generator.ts
@@ -1090,9 +1090,22 @@ ${acc.routeTree.map((child) => `${child.variableName}Route: typeof ${getResolved
})
if (transformResult.result === 'no-route-export') {
- this.logger.warn(
- `Route file "${node.fullPath}" does not contain any route piece. This is likely a mistake.`,
- )
+ const fileName = path.basename(node.fullPath)
+ const dirName = path.dirname(node.fullPath)
+ const ignorePrefix = this.config.routeFileIgnorePrefix
+ const ignorePattern = this.config.routeFileIgnorePattern
+ const suggestedFileName = `${ignorePrefix}${fileName}`
+ const suggestedFullPath = path.join(dirName, suggestedFileName)
+
+ let message = `Warning: Route file "${node.fullPath}" does not export a Route. This file will not be included in the route tree.`
+ message += `\n\nIf this file is not intended to be a route, you can exclude it using one of these options:`
+ message += `\n 1. Rename the file to "${suggestedFullPath}" (prefix with "${ignorePrefix}")`
+ message += `\n 2. Use 'routeFileIgnorePattern' in your config to match this file`
+ message += `\n\nCurrent configuration:`
+ message += `\n routeFileIgnorePrefix: "${ignorePrefix}"`
+ message += `\n routeFileIgnorePattern: ${ignorePattern ? `"${ignorePattern}"` : 'undefined'}`
+
+ this.logger.warn(message)
return null
}
if (transformResult.result === 'error') {
diff --git a/packages/router-generator/tests/generator.test.ts b/packages/router-generator/tests/generator.test.ts
index 42594d7b0c0..40c66e30ed4 100644
--- a/packages/router-generator/tests/generator.test.ts
+++ b/packages/router-generator/tests/generator.test.ts
@@ -104,6 +104,18 @@ function rewriteConfigByFolderName(folderName: string, config: Config) {
case 'virtual-config-file-default-export':
config.virtualRouteConfig = './routes.ts'
break
+ case 'virtual-physical-empty-path-merge':
+ config.virtualRouteConfig = './routes.ts'
+ break
+ case 'virtual-physical-empty-path-conflict-root':
+ config.virtualRouteConfig = './routes.ts'
+ break
+ case 'virtual-physical-empty-path-conflict-virtual':
+ config.virtualRouteConfig = './routes.ts'
+ break
+ case 'virtual-physical-no-prefix':
+ config.virtualRouteConfig = './routes.ts'
+ break
case 'virtual-with-escaped-underscore':
{
// Test case for escaped underscores in physical routes mounted via virtual config
@@ -243,6 +255,12 @@ function shouldThrow(folderName: string) {
if (folderName === 'duplicate-fullPath') {
return `Conflicting configuration paths were found for the following routes: "/", "/".`
}
+ if (folderName === 'virtual-physical-empty-path-conflict-root') {
+ return `Conflicting configuration paths were found for the following routes: "/__root", "/__root".`
+ }
+ if (folderName === 'virtual-physical-empty-path-conflict-virtual') {
+ return `Conflicting configuration paths were found for the following routes: "/about", "/about".`
+ }
return undefined
}
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes.ts b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes.ts
new file mode 100644
index 00000000000..c2b6660fc3c
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes.ts
@@ -0,0 +1,5 @@
+import { physical, rootRoute } from '@tanstack/virtual-file-routes'
+
+// This test verifies that a __root.tsx in a physical directory mounted at root
+// produces a proper conflict error with the virtual root
+export const routes = rootRoute('__root.tsx', [physical('', 'merged')])
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/__root.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/__root.tsx
new file mode 100644
index 00000000000..f463b796b44
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/__root.tsx
@@ -0,0 +1,5 @@
+import { createRootRoute, Outlet } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: () => ,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/merged/index.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/merged/index.tsx
new file mode 100644
index 00000000000..bad00da72c9
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/merged/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: () =>
Index
,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/merged/route.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/merged/route.tsx
new file mode 100644
index 00000000000..a164afcb81c
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/routes/merged/route.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+// This route.tsx in a physical directory mounted at root level
+// conflicts with the virtual root __root.tsx - can't have two root routes
+export const Route = createFileRoute('')({})
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/tsr.config.json b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/tsr.config.json
new file mode 100644
index 00000000000..4d587108e38
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-root/tsr.config.json
@@ -0,0 +1,5 @@
+{
+ "routesDirectory": "./routes",
+ "generatedRouteTree": "./routeTree.gen.ts",
+ "virtualRouteConfig": "./routes.ts"
+}
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes.ts b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes.ts
new file mode 100644
index 00000000000..1a6c713d514
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes.ts
@@ -0,0 +1,8 @@
+import { physical, rootRoute, route } from '@tanstack/virtual-file-routes'
+
+// This test verifies that a virtual route path conflicts with
+// a physical route path when using empty path prefix
+export const routes = rootRoute('__root.tsx', [
+ route('/about', 'about.tsx'), // virtual /about
+ physical('', 'merged'), // physical also has about.tsx -> /about
+])
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/__root.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/__root.tsx
new file mode 100644
index 00000000000..f463b796b44
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/__root.tsx
@@ -0,0 +1,5 @@
+import { createRootRoute, Outlet } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: () => ,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/about.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/about.tsx
new file mode 100644
index 00000000000..47bedea6c8c
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/about.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+// Virtual about route - conflicts with merged/about.tsx
+export const Route = createFileRoute('/about')({
+ component: () => About (virtual)
,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/merged/about.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/merged/about.tsx
new file mode 100644
index 00000000000..6b15c67503c
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/merged/about.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+// Physical about route - conflicts with virtual about.tsx -> /about
+export const Route = createFileRoute('/about')({
+ component: () => About (physical)
,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/merged/index.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/merged/index.tsx
new file mode 100644
index 00000000000..bad00da72c9
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/routes/merged/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: () => Index
,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/tsr.config.json b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/tsr.config.json
new file mode 100644
index 00000000000..4d587108e38
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-conflict-virtual/tsr.config.json
@@ -0,0 +1,5 @@
+{
+ "routesDirectory": "./routes",
+ "generatedRouteTree": "./routeTree.gen.ts",
+ "virtualRouteConfig": "./routes.ts"
+}
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routeTree.snapshot.ts
new file mode 100644
index 00000000000..6de1ed69756
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routeTree.snapshot.ts
@@ -0,0 +1,95 @@
+/* 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 ContactRouteImport } from './routes/merged/contact'
+import { Route as aboutRouteImport } from './routes/about'
+import { Route as IndexRouteImport } from './routes/merged/index'
+
+const ContactRoute = ContactRouteImport.update({
+ id: '/contact',
+ path: '/contact',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const aboutRoute = aboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/about': typeof aboutRoute
+ '/contact': typeof ContactRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/about': typeof aboutRoute
+ '/contact': typeof ContactRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/about': typeof aboutRoute
+ '/contact': typeof ContactRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/about' | '/contact'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/about' | '/contact'
+ id: '__root__' | '/' | '/about' | '/contact'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ aboutRoute: typeof aboutRoute
+ ContactRoute: typeof ContactRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/contact': {
+ id: '/contact'
+ path: '/contact'
+ fullPath: '/contact'
+ preLoaderRoute: typeof ContactRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof aboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ aboutRoute: aboutRoute,
+ ContactRoute: ContactRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes.ts b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes.ts
new file mode 100644
index 00000000000..c429574e1a6
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes.ts
@@ -0,0 +1,8 @@
+import { physical, rootRoute, route } from '@tanstack/virtual-file-routes'
+
+export const routes = rootRoute('__root.tsx', [
+ // Virtual route defined here
+ route('/about', 'about.tsx'),
+ // Physical mount with empty path - should merge into root level
+ physical('', 'merged'),
+])
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/__root.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/__root.tsx
new file mode 100644
index 00000000000..9c657c7d5b4
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/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-physical-empty-path-merge/routes/about.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/about.tsx
new file mode 100644
index 00000000000..e56767c8ad8
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/about.tsx
@@ -0,0 +1,2 @@
+import { createFileRoute } from '@tanstack/react-router'
+export const Route = createFileRoute('/about')()
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/merged/contact.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/merged/contact.tsx
new file mode 100644
index 00000000000..12a392def8d
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/merged/contact.tsx
@@ -0,0 +1,2 @@
+import { createFileRoute } from '@tanstack/react-router'
+export const Route = createFileRoute('/contact')()
diff --git a/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/merged/index.tsx b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/merged/index.tsx
new file mode 100644
index 00000000000..d1d6296babb
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/routes/merged/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-physical-empty-path-merge/tsr.config.json b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/tsr.config.json
new file mode 100644
index 00000000000..4d587108e38
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-empty-path-merge/tsr.config.json
@@ -0,0 +1,5 @@
+{
+ "routesDirectory": "./routes",
+ "generatedRouteTree": "./routeTree.gen.ts",
+ "virtualRouteConfig": "./routes.ts"
+}
diff --git a/packages/router-generator/tests/generator/virtual-physical-no-prefix/routeTree.snapshot.ts b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routeTree.snapshot.ts
new file mode 100644
index 00000000000..6de1ed69756
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routeTree.snapshot.ts
@@ -0,0 +1,95 @@
+/* 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 ContactRouteImport } from './routes/merged/contact'
+import { Route as aboutRouteImport } from './routes/about'
+import { Route as IndexRouteImport } from './routes/merged/index'
+
+const ContactRoute = ContactRouteImport.update({
+ id: '/contact',
+ path: '/contact',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const aboutRoute = aboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/about': typeof aboutRoute
+ '/contact': typeof ContactRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/about': typeof aboutRoute
+ '/contact': typeof ContactRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/about': typeof aboutRoute
+ '/contact': typeof ContactRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/about' | '/contact'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/about' | '/contact'
+ id: '__root__' | '/' | '/about' | '/contact'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ aboutRoute: typeof aboutRoute
+ ContactRoute: typeof ContactRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/contact': {
+ id: '/contact'
+ path: '/contact'
+ fullPath: '/contact'
+ preLoaderRoute: typeof ContactRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof aboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ aboutRoute: aboutRoute,
+ ContactRoute: ContactRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
diff --git a/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes.ts b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes.ts
new file mode 100644
index 00000000000..0fa8033c6d3
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes.ts
@@ -0,0 +1,8 @@
+import { physical, rootRoute, route } from '@tanstack/virtual-file-routes'
+
+// This test verifies the single-argument physical() overload
+// which uses an empty path prefix (merges at current level)
+export const routes = rootRoute('__root.tsx', [
+ route('/about', 'about.tsx'),
+ physical('merged'), // Single argument - merges at root
+])
diff --git a/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/__root.tsx b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/__root.tsx
new file mode 100644
index 00000000000..f463b796b44
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/__root.tsx
@@ -0,0 +1,5 @@
+import { createRootRoute, Outlet } from '@tanstack/react-router'
+
+export const Route = createRootRoute({
+ component: () => ,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/about.tsx b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/about.tsx
new file mode 100644
index 00000000000..51df2dd30ea
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/about.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/about')({
+ component: () => About
,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/merged/contact.tsx b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/merged/contact.tsx
new file mode 100644
index 00000000000..77fd4df532e
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/merged/contact.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/contact')({
+ component: () => Contact
,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/merged/index.tsx b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/merged/index.tsx
new file mode 100644
index 00000000000..bad00da72c9
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-no-prefix/routes/merged/index.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ component: () => Index
,
+})
diff --git a/packages/router-generator/tests/generator/virtual-physical-no-prefix/tsr.config.json b/packages/router-generator/tests/generator/virtual-physical-no-prefix/tsr.config.json
new file mode 100644
index 00000000000..4d587108e38
--- /dev/null
+++ b/packages/router-generator/tests/generator/virtual-physical-no-prefix/tsr.config.json
@@ -0,0 +1,5 @@
+{
+ "routesDirectory": "./routes",
+ "generatedRouteTree": "./routeTree.gen.ts",
+ "virtualRouteConfig": "./routes.ts"
+}
diff --git a/packages/virtual-file-routes/src/api.ts b/packages/virtual-file-routes/src/api.ts
index dbcaee7a366..f85c6ee44c8 100644
--- a/packages/virtual-file-routes/src/api.ts
+++ b/packages/virtual-file-routes/src/api.ts
@@ -83,13 +83,36 @@ export function route(
}
}
+/**
+ * Mount a physical directory of route files at a given path prefix.
+ *
+ * @param pathPrefix - The path prefix to mount the directory at. Use empty string '' to merge routes at the current level.
+ * @param directory - The directory containing the route files, relative to the routes directory.
+ */
+export function physical(pathPrefix: string, directory: string): PhysicalSubtree
+/**
+ * Mount a physical directory of route files at the current level (empty path prefix).
+ * This is equivalent to `physical('', directory)`.
+ *
+ * @param directory - The directory containing the route files, relative to the routes directory.
+ */
+export function physical(directory: string): PhysicalSubtree
export function physical(
- pathPrefix: string,
- directory: string,
+ pathPrefixOrDirectory: string,
+ directory?: string,
): PhysicalSubtree {
+ if (directory === undefined) {
+ // Single argument: directory only, use empty path prefix
+ return {
+ type: 'physical',
+ directory: pathPrefixOrDirectory,
+ pathPrefix: '',
+ }
+ }
+ // Two arguments: pathPrefix and directory
return {
type: 'physical',
directory,
- pathPrefix,
+ pathPrefix: pathPrefixOrDirectory,
}
}