@@ -59,7 +59,7 @@ export function generateRoutes(ctx: Context): Array<VirtualFile> {
59
59
// precompute
60
60
const fileToRoutes = new Map < string , Set < string > > ( ) ;
61
61
const lineages = new Map < string , Array < RouteManifestEntry > > ( ) ;
62
- const pages = new Set < string > ( ) ;
62
+ const allPages = new Set < string > ( ) ;
63
63
const routeToPages = new Map < string , Set < string > > ( ) ;
64
64
for ( const route of Object . values ( ctx . config . routes ) ) {
65
65
// fileToRoutes
@@ -75,9 +75,10 @@ export function generateRoutes(ctx: Context): Array<VirtualFile> {
75
75
lineages . set ( route . id , lineage ) ;
76
76
77
77
// pages
78
- const page = Route . fullpath ( lineage ) ;
79
- if ( ! page ) continue ;
80
- pages . add ( page ) ;
78
+ const fullpath = Route . fullpath ( lineage ) ;
79
+ if ( ! fullpath ) continue ;
80
+ const pages = explodeOptionalSegments ( fullpath ) ;
81
+ pages . forEach ( ( page ) => allPages . add ( page ) ) ;
81
82
82
83
// routePages
83
84
lineage . forEach ( ( { id } ) => {
@@ -86,7 +87,7 @@ export function generateRoutes(ctx: Context): Array<VirtualFile> {
86
87
routePages = new Set < string > ( ) ;
87
88
routeToPages . set ( id , routePages ) ;
88
89
}
89
- routePages . add ( page ) ;
90
+ pages . forEach ( ( page ) => routePages . add ( page ) ) ;
90
91
} ) ;
91
92
}
92
93
@@ -107,7 +108,7 @@ export function generateRoutes(ctx: Context): Array<VirtualFile> {
107
108
}
108
109
` +
109
110
"\n\n" +
110
- Babel . generate ( pagesType ( pages ) ) . code +
111
+ Babel . generate ( pagesType ( allPages ) ) . code +
111
112
"\n\n" +
112
113
Babel . generate ( routeFilesType ( { fileToRoutes, routeToPages } ) ) . code ,
113
114
} ;
@@ -346,3 +347,49 @@ function paramsType(path: string) {
346
347
} )
347
348
) ;
348
349
}
350
+
351
+ // https://github.com/remix-run/react-router/blob/7a7f4b11ca8b26889ad328ba0ee5a749b0c6939e/packages/react-router/lib/router/utils.ts#L894C1-L937C2
352
+ function explodeOptionalSegments ( path : string ) : string [ ] {
353
+ let segments = path . split ( "/" ) ;
354
+ if ( segments . length === 0 ) return [ ] ;
355
+
356
+ let [ first , ...rest ] = segments ;
357
+
358
+ // Optional path segments are denoted by a trailing `?`
359
+ let isOptional = first . endsWith ( "?" ) ;
360
+ // Compute the corresponding required segment: `foo?` -> `foo`
361
+ let required = first . replace ( / \? $ / , "" ) ;
362
+
363
+ if ( rest . length === 0 ) {
364
+ // Interpret empty string as omitting an optional segment
365
+ // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
366
+ return isOptional ? [ required , "" ] : [ required ] ;
367
+ }
368
+
369
+ let restExploded = explodeOptionalSegments ( rest . join ( "/" ) ) ;
370
+
371
+ let result : string [ ] = [ ] ;
372
+
373
+ // All child paths with the prefix. Do this for all children before the
374
+ // optional version for all children, so we get consistent ordering where the
375
+ // parent optional aspect is preferred as required. Otherwise, we can get
376
+ // child sections interspersed where deeper optional segments are higher than
377
+ // parent optional segments, where for example, /:two would explode _earlier_
378
+ // then /:one. By always including the parent as required _for all children_
379
+ // first, we avoid this issue
380
+ result . push (
381
+ ...restExploded . map ( ( subpath ) =>
382
+ subpath === "" ? required : [ required , subpath ] . join ( "/" )
383
+ )
384
+ ) ;
385
+
386
+ // Then, if this is an optional value, add all child versions without
387
+ if ( isOptional ) {
388
+ result . push ( ...restExploded ) ;
389
+ }
390
+
391
+ // for absolute paths, ensure `/` instead of empty segment
392
+ return result . map ( ( exploded ) =>
393
+ path . startsWith ( "/" ) && exploded === "" ? "/" : exploded
394
+ ) ;
395
+ }
0 commit comments