Skip to content

Commit a82bfa1

Browse files
feat(router-generator): allow virtual file routes without file names (#2500)
1 parent ac39caa commit a82bfa1

11 files changed

Lines changed: 185 additions & 73 deletions

File tree

docs/framework/react/guide/virtual-file-routes.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,19 @@ const virtualRouteConfig = rootRoute('root.tsx', [
9999
])
100100
```
101101

102+
You can also define a virtual route without a file name. This allows to set a common path prefix for its children:
103+
104+
```tsx
105+
import { route } from '@tanstack/virtual-file-routes'
106+
107+
const virtualRouteConfig = rootRoute('root.tsx', [
108+
route('/hello', [
109+
route('/world', 'world.tsx'), // full path will be "/hello/world"
110+
route('/universe', 'universe.tsx'), // full path will be "/hello/universe"
111+
]),
112+
])
113+
```
114+
102115
## Virtual Index Route
103116

104117
The `index` function is used to create a virtual index route. It takes a file name. Here's an example of a virtual index route:

examples/react/basic-virtual-file-based/routes.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ export const routes = rootRoute('root.tsx', [
1313
route('$postId', 'posts/posts-detail.tsx'),
1414
]),
1515
layout('first', 'layout/first-layout.tsx', [
16-
layout('second', 'layout/second-layout.tsx', [
17-
route('/layout-a', 'a.tsx'),
18-
route('/layout-b', 'b.tsx'),
16+
layout('layout/second-layout.tsx', [
17+
route('route-without-file', [
18+
route('/layout-a', 'a.tsx'),
19+
route('/layout-b', 'b.tsx'),
20+
]),
1921
]),
2022
]),
2123
physical('/classic', 'file-based-subtree'),

examples/react/basic-virtual-file-based/src/routeTree.gen.ts

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
// This file is auto-generated by TanStack Router
1010

11+
import { createFileRoute } from '@tanstack/react-router'
12+
1113
// Import Routes
1214

1315
import { Route as rootRoute } from './routes/root'
@@ -24,6 +26,10 @@ import { Route as ClassicHelloUniverseImport } from './routes/file-based-subtree
2426
import { Route as bImport } from './routes/b'
2527
import { Route as aImport } from './routes/a'
2628

29+
// Create Virtual Routes
30+
31+
const Import = createFileRoute('/_first/_second-layout/route-without-file')()
32+
2733
// Create/Update Routes
2834

2935
const postsPostsRoute = postsPostsImport.update({
@@ -47,7 +53,7 @@ const postsPostsDetailRoute = postsPostsDetailImport.update({
4753
} as any)
4854

4955
const layoutSecondLayoutRoute = layoutSecondLayoutImport.update({
50-
id: '/_second',
56+
id: '/_second-layout',
5157
getParentRoute: () => layoutFirstLayoutRoute,
5258
} as any)
5359

@@ -76,14 +82,19 @@ const ClassicHelloUniverseRoute = ClassicHelloUniverseImport.update({
7682
getParentRoute: () => ClassicHelloRouteRoute,
7783
} as any)
7884

85+
const Route = Import.update({
86+
path: '/route-without-file',
87+
getParentRoute: () => layoutSecondLayoutRoute,
88+
} as any)
89+
7990
const bRoute = bImport.update({
8091
path: '/layout-b',
81-
getParentRoute: () => layoutSecondLayoutRoute,
92+
getParentRoute: () => Route,
8293
} as any)
8394

8495
const aRoute = aImport.update({
8596
path: '/layout-a',
86-
getParentRoute: () => layoutSecondLayoutRoute,
97+
getParentRoute: () => Route,
8798
} as any)
8899

89100
// Populate the FileRoutesByPath interface
@@ -125,8 +136,8 @@ declare module '@tanstack/react-router' {
125136
preLoaderRoute: typeof postsPostsHomeImport
126137
parentRoute: typeof postsPostsImport
127138
}
128-
'/_first/_second': {
129-
id: '/_first/_second'
139+
'/_first/_second-layout': {
140+
id: '/_first/_second-layout'
130141
path: ''
131142
fullPath: ''
132143
preLoaderRoute: typeof layoutSecondLayoutImport
@@ -139,18 +150,11 @@ declare module '@tanstack/react-router' {
139150
preLoaderRoute: typeof postsPostsDetailImport
140151
parentRoute: typeof postsPostsImport
141152
}
142-
'/_first/_second/layout-a': {
143-
id: '/_first/_second/layout-a'
144-
path: '/layout-a'
145-
fullPath: '/layout-a'
146-
preLoaderRoute: typeof aImport
147-
parentRoute: typeof layoutSecondLayoutImport
148-
}
149-
'/_first/_second/layout-b': {
150-
id: '/_first/_second/layout-b'
151-
path: '/layout-b'
152-
fullPath: '/layout-b'
153-
preLoaderRoute: typeof bImport
153+
'/_first/_second-layout/route-without-file': {
154+
id: '/_first/_second-layout/route-without-file'
155+
path: '/route-without-file'
156+
fullPath: '/route-without-file'
157+
preLoaderRoute: typeof Import
154158
parentRoute: typeof layoutSecondLayoutImport
155159
}
156160
'/classic/hello/universe': {
@@ -174,21 +178,45 @@ declare module '@tanstack/react-router' {
174178
preLoaderRoute: typeof ClassicHelloIndexImport
175179
parentRoute: typeof ClassicHelloRouteImport
176180
}
181+
'/_first/_second-layout/route-without-file/layout-a': {
182+
id: '/_first/_second-layout/route-without-file/layout-a'
183+
path: '/layout-a'
184+
fullPath: '/route-without-file/layout-a'
185+
preLoaderRoute: typeof aImport
186+
parentRoute: typeof rootRoute
187+
}
188+
'/_first/_second-layout/route-without-file/layout-b': {
189+
id: '/_first/_second-layout/route-without-file/layout-b'
190+
path: '/layout-b'
191+
fullPath: '/route-without-file/layout-b'
192+
preLoaderRoute: typeof bImport
193+
parentRoute: typeof rootRoute
194+
}
177195
}
178196
}
179197

180198
// Create and export the route tree
181199

182-
interface layoutSecondLayoutRouteChildren {
200+
interface RouteChildren {
183201
aRoute: typeof aRoute
184202
bRoute: typeof bRoute
185203
}
186204

187-
const layoutSecondLayoutRouteChildren: layoutSecondLayoutRouteChildren = {
205+
const RouteChildren: RouteChildren = {
188206
aRoute: aRoute,
189207
bRoute: bRoute,
190208
}
191209

210+
const RouteWithChildren = Route._addFileChildren(RouteChildren)
211+
212+
interface layoutSecondLayoutRouteChildren {
213+
Route: typeof RouteWithChildren
214+
}
215+
216+
const layoutSecondLayoutRouteChildren: layoutSecondLayoutRouteChildren = {
217+
Route: RouteWithChildren,
218+
}
219+
192220
const layoutSecondLayoutRouteWithChildren =
193221
layoutSecondLayoutRoute._addFileChildren(layoutSecondLayoutRouteChildren)
194222

@@ -239,23 +267,25 @@ export interface FileRoutesByFullPath {
239267
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
240268
'/posts/': typeof postsPostsHomeRoute
241269
'/posts/$postId': typeof postsPostsDetailRoute
242-
'/layout-a': typeof aRoute
243-
'/layout-b': typeof bRoute
270+
'/route-without-file': typeof RouteWithChildren
244271
'/classic/hello/universe': typeof ClassicHelloUniverseRoute
245272
'/classic/hello/world': typeof ClassicHelloWorldRoute
246273
'/classic/hello/': typeof ClassicHelloIndexRoute
274+
'/route-without-file/layout-a': typeof aRoute
275+
'/route-without-file/layout-b': typeof bRoute
247276
}
248277

249278
export interface FileRoutesByTo {
250279
'/': typeof homeRoute
251280
'': typeof layoutSecondLayoutRouteWithChildren
252281
'/posts': typeof postsPostsHomeRoute
253282
'/posts/$postId': typeof postsPostsDetailRoute
254-
'/layout-a': typeof aRoute
255-
'/layout-b': typeof bRoute
283+
'/route-without-file': typeof RouteWithChildren
256284
'/classic/hello/universe': typeof ClassicHelloUniverseRoute
257285
'/classic/hello/world': typeof ClassicHelloWorldRoute
258286
'/classic/hello': typeof ClassicHelloIndexRoute
287+
'/route-without-file/layout-a': typeof aRoute
288+
'/route-without-file/layout-b': typeof bRoute
259289
}
260290

261291
export interface FileRoutesById {
@@ -265,13 +295,14 @@ export interface FileRoutesById {
265295
'/posts': typeof postsPostsRouteWithChildren
266296
'/classic/hello': typeof ClassicHelloRouteRouteWithChildren
267297
'/posts/': typeof postsPostsHomeRoute
268-
'/_first/_second': typeof layoutSecondLayoutRouteWithChildren
298+
'/_first/_second-layout': typeof layoutSecondLayoutRouteWithChildren
269299
'/posts/$postId': typeof postsPostsDetailRoute
270-
'/_first/_second/layout-a': typeof aRoute
271-
'/_first/_second/layout-b': typeof bRoute
300+
'/_first/_second-layout/route-without-file': typeof RouteWithChildren
272301
'/classic/hello/universe': typeof ClassicHelloUniverseRoute
273302
'/classic/hello/world': typeof ClassicHelloWorldRoute
274303
'/classic/hello/': typeof ClassicHelloIndexRoute
304+
'/_first/_second-layout/route-without-file/layout-a': typeof aRoute
305+
'/_first/_second-layout/route-without-file/layout-b': typeof bRoute
275306
}
276307

277308
export interface FileRouteTypes {
@@ -283,36 +314,39 @@ export interface FileRouteTypes {
283314
| '/classic/hello'
284315
| '/posts/'
285316
| '/posts/$postId'
286-
| '/layout-a'
287-
| '/layout-b'
317+
| '/route-without-file'
288318
| '/classic/hello/universe'
289319
| '/classic/hello/world'
290320
| '/classic/hello/'
321+
| '/route-without-file/layout-a'
322+
| '/route-without-file/layout-b'
291323
fileRoutesByTo: FileRoutesByTo
292324
to:
293325
| '/'
294326
| ''
295327
| '/posts'
296328
| '/posts/$postId'
297-
| '/layout-a'
298-
| '/layout-b'
329+
| '/route-without-file'
299330
| '/classic/hello/universe'
300331
| '/classic/hello/world'
301332
| '/classic/hello'
333+
| '/route-without-file/layout-a'
334+
| '/route-without-file/layout-b'
302335
id:
303336
| '__root__'
304337
| '/'
305338
| '/_first'
306339
| '/posts'
307340
| '/classic/hello'
308341
| '/posts/'
309-
| '/_first/_second'
342+
| '/_first/_second-layout'
310343
| '/posts/$postId'
311-
| '/_first/_second/layout-a'
312-
| '/_first/_second/layout-b'
344+
| '/_first/_second-layout/route-without-file'
313345
| '/classic/hello/universe'
314346
| '/classic/hello/world'
315347
| '/classic/hello/'
348+
| '/_first/_second-layout/route-without-file/layout-a'
349+
| '/_first/_second-layout/route-without-file/layout-b'
316350
fileRoutesById: FileRoutesById
317351
}
318352

@@ -354,7 +388,7 @@ export const routeTree = rootRoute
354388
"/_first": {
355389
"filePath": "layout/first-layout.tsx",
356390
"children": [
357-
"/_first/_second"
391+
"/_first/_second-layout"
358392
]
359393
},
360394
"/posts": {
@@ -376,25 +410,24 @@ export const routeTree = rootRoute
376410
"filePath": "posts/posts-home.tsx",
377411
"parent": "/posts"
378412
},
379-
"/_first/_second": {
413+
"/_first/_second-layout": {
380414
"filePath": "layout/second-layout.tsx",
381415
"parent": "/_first",
382416
"children": [
383-
"/_first/_second/layout-a",
384-
"/_first/_second/layout-b"
417+
"/_first/_second-layout/route-without-file"
385418
]
386419
},
387420
"/posts/$postId": {
388421
"filePath": "posts/posts-detail.tsx",
389422
"parent": "/posts"
390423
},
391-
"/_first/_second/layout-a": {
392-
"filePath": "a.tsx",
393-
"parent": "/_first/_second"
394-
},
395-
"/_first/_second/layout-b": {
396-
"filePath": "b.tsx",
397-
"parent": "/_first/_second"
424+
"/_first/_second-layout/route-without-file": {
425+
"filePath": "",
426+
"parent": "/_first/_second-layout",
427+
"children": [
428+
"/_first/_second-layout/route-without-file/layout-a",
429+
"/_first/_second-layout/route-without-file/layout-b"
430+
]
398431
},
399432
"/classic/hello/universe": {
400433
"filePath": "file-based-subtree/hello/universe.tsx",
@@ -407,6 +440,14 @@ export const routeTree = rootRoute
407440
"/classic/hello/": {
408441
"filePath": "file-based-subtree/hello/index.tsx",
409442
"parent": "/classic/hello"
443+
},
444+
"/_first/_second-layout/route-without-file/layout-a": {
445+
"filePath": "a.tsx",
446+
"parent": "/_first/_second-layout/route-without-file"
447+
},
448+
"/_first/_second-layout/route-without-file/layout-b": {
449+
"filePath": "b.tsx",
450+
"parent": "/_first/_second-layout/route-without-file"
410451
}
411452
}
412453
}

examples/react/basic-virtual-file-based/src/routes/a.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createFileRoute } from '@tanstack/react-router'
22

3-
export const Route = createFileRoute('/_first/_second/layout-a')({
3+
export const Route = createFileRoute(
4+
'/_first/_second-layout/route-without-file/layout-a',
5+
)({
46
component: LayoutAComponent,
57
})
68

examples/react/basic-virtual-file-based/src/routes/b.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createFileRoute } from '@tanstack/react-router'
22

3-
export const Route = createFileRoute('/_first/_second/layout-b')({
3+
export const Route = createFileRoute(
4+
'/_first/_second-layout/route-without-file/layout-b',
5+
)({
46
component: LayoutBComponent,
57
})
68

examples/react/basic-virtual-file-based/src/routes/layout/second-layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Link, Outlet, createFileRoute } from '@tanstack/react-router'
22

3-
export const Route = createFileRoute('/_first/_second')({
3+
export const Route = createFileRoute('/_first/_second-layout')({
44
component: LayoutComponent,
55
})
66

@@ -10,15 +10,15 @@ function LayoutComponent() {
1010
<div>I'm a nested layout</div>
1111
<div className="flex gap-2 border-b">
1212
<Link
13-
to="/layout-a"
13+
to="/route-without-file/layout-a"
1414
activeProps={{
1515
className: 'font-bold',
1616
}}
1717
>
1818
Layout A
1919
</Link>
2020
<Link
21-
to="/layout-b"
21+
to="/route-without-file/layout-b"
2222
activeProps={{
2323
className: 'font-bold',
2424
}}

examples/react/basic-virtual-file-based/src/routes/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function RootComponent() {
3636
Posts
3737
</Link>{' '}
3838
<Link
39-
to="/layout-a"
39+
to="/route-without-file/layout-a"
4040
activeProps={{
4141
className: 'font-bold',
4242
}}

packages/router-generator/src/filesystem/virtual/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ const indexRouteSchema = z.object({
1313

1414
const layoutRouteSchema: z.ZodType<LayoutRoute> = z.object({
1515
type: z.literal('layout'),
16-
id: z.string(),
16+
id: z.string().optional(),
1717
file: z.string(),
1818
children: z.array(z.lazy(() => virtualRouteNodeSchema)).optional(),
1919
})
2020

2121
const routeSchema: z.ZodType<Route> = z.object({
2222
type: z.literal('route'),
23-
file: z.string(),
23+
file: z.string().optional(),
2424
path: z.string(),
2525
children: z.array(z.lazy(() => virtualRouteNodeSchema)).optional(),
2626
})

0 commit comments

Comments
 (0)