6
6
import type { NextConfigComplete } from '../../config-shared'
7
7
import { isParallelRouteSegment } from '../../../shared/lib/segment'
8
8
import fs from 'fs'
9
- import { generateRouteTypesFile } from './typegen'
9
+ import { generateRouteTypesFile , generateLinkTypesFile } from './typegen'
10
10
import { tryToParsePath } from '../../../lib/try-to-parse-path'
11
11
12
12
interface RouteInfo {
@@ -27,46 +27,71 @@ export interface RouteTypesManifest {
27
27
// Convert a custom-route source string (`/blog/:slug`, `/docs/:path*`, ...)
28
28
// into the bracket-syntax used by other Next.js route helpers so that we can
29
29
// reuse `getRouteRegex()` to extract groups.
30
- export function convertCustomRouteSource ( source : string ) : string {
30
+ export function convertCustomRouteSource ( source : string ) : string [ ] {
31
31
const parseResult = tryToParsePath ( source )
32
32
33
33
if ( parseResult . error || ! parseResult . tokens ) {
34
34
// Fallback to original source if parsing fails
35
- return source . startsWith ( '/' ) ? source : '/' + source
35
+ return source . startsWith ( '/' ) ? [ source ] : [ '/' + source ]
36
36
}
37
37
38
- let result = ''
38
+ const possibleNormalizedRoutes = [ '' ]
39
+ let slugCnt = 1
40
+
41
+ function append ( suffix : string ) {
42
+ for ( let i = 0 ; i < possibleNormalizedRoutes . length ; i ++ ) {
43
+ possibleNormalizedRoutes [ i ] += suffix
44
+ }
45
+ }
46
+
47
+ function fork ( suffix : string ) {
48
+ const currentLength = possibleNormalizedRoutes . length
49
+ for ( let i = 0 ; i < currentLength ; i ++ ) {
50
+ possibleNormalizedRoutes . push ( possibleNormalizedRoutes [ i ] + suffix )
51
+ }
52
+ }
39
53
40
54
for ( const token of parseResult . tokens ) {
41
- if ( typeof token === 'string' ) {
42
- // Literal path segment
43
- result += token
44
- } else {
45
- // Parameter token
46
- const { name, modifier, prefix } = token
47
-
48
- // Add the prefix (usually '/')
49
- result += prefix
50
-
51
- if ( modifier === '*' ) {
52
- // Catch-all zero or more: :param* -> [[...param]]
53
- result += `[[...${ name } ]]`
54
- } else if ( modifier === '+' ) {
55
- // Catch-all one or more: :param+ -> [...param]
56
- result += `[...${ name } ]`
57
- } else if ( modifier === '?' ) {
58
- // Optional catch-all: :param? -> [[...param]]
59
- result += `[[...${ name } ]]`
60
- } else {
61
- // Standard dynamic segment: :param -> [param]
62
- result += `[${ name } ]`
55
+ if ( typeof token === 'object' ) {
56
+ // Make sure the slug is always named.
57
+ const slug = token . name || ( slugCnt ++ === 1 ? 'slug' : `slug${ slugCnt } ` )
58
+ if ( token . modifier === '*' ) {
59
+ append ( `${ token . prefix } [[...${ slug } ]]` )
60
+ } else if ( token . modifier === '+' ) {
61
+ append ( `${ token . prefix } [...${ slug } ]` )
62
+ } else if ( token . modifier === '' ) {
63
+ if ( token . pattern === '[^\\/#\\?]+?' ) {
64
+ // A safe slug
65
+ append ( `${ token . prefix } [${ slug } ]` )
66
+ } else if ( token . pattern === '.*' ) {
67
+ // An optional catch-all slug
68
+ append ( `${ token . prefix } [[...${ slug } ]]` )
69
+ } else if ( token . pattern === '.+' ) {
70
+ // A catch-all slug
71
+ append ( `${ token . prefix } [...${ slug } ]` )
72
+ } else {
73
+ // Other regex patterns are not supported. Skip this route.
74
+ return [ ]
75
+ }
76
+ } else if ( token . modifier === '?' ) {
77
+ if ( / ^ [ a - z A - Z 0 - 9 _ / ] * $ / . test ( token . pattern ) ) {
78
+ // An optional slug with plain text only, fork the route.
79
+ append ( token . prefix )
80
+ fork ( token . pattern )
81
+ } else {
82
+ // Optional modifier `?` and regex patterns are not supported.
83
+ return [ ]
84
+ }
63
85
}
86
+ } else if ( typeof token === 'string' ) {
87
+ append ( token )
64
88
}
65
89
}
66
90
67
91
// Ensure leading slash
68
- if ( ! result . startsWith ( '/' ) ) result = '/' + result
69
- return result
92
+ return possibleNormalizedRoutes . map ( ( route ) =>
93
+ route . startsWith ( '/' ) ? route : '/' + route
94
+ )
70
95
}
71
96
72
97
/**
@@ -165,11 +190,12 @@ export async function createRouteTypesManifest({
165
190
const rd = await redirects ( )
166
191
167
192
for ( const item of rd ) {
168
- const source = convertCustomRouteSource ( item . source )
169
-
170
- manifest . redirectRoutes [ source ] = {
171
- path : source ,
172
- groups : extractRouteParams ( source ) ,
193
+ const possibleRoutes = convertCustomRouteSource ( item . source )
194
+ for ( const route of possibleRoutes ) {
195
+ manifest . redirectRoutes [ route ] = {
196
+ path : route ,
197
+ groups : extractRouteParams ( route ) ,
198
+ }
173
199
}
174
200
}
175
201
}
@@ -187,10 +213,12 @@ export async function createRouteTypesManifest({
187
213
]
188
214
189
215
for ( const item of allSources ) {
190
- const source = convertCustomRouteSource ( item . source )
191
- manifest . rewriteRoutes [ source ] = {
192
- path : source ,
193
- groups : extractRouteParams ( source ) ,
216
+ const possibleRoutes = convertCustomRouteSource ( item . source )
217
+ for ( const route of possibleRoutes ) {
218
+ manifest . rewriteRoutes [ route ] = {
219
+ path : route ,
220
+ groups : extractRouteParams ( route ) ,
221
+ }
194
222
}
195
223
}
196
224
}
@@ -209,8 +237,12 @@ export async function writeRouteTypesManifest(
209
237
await fs . promises . mkdir ( dirname , { recursive : true } )
210
238
}
211
239
212
- await fs . promises . writeFile (
213
- filePath ,
214
- generateRouteTypesFile ( manifest , config )
215
- )
240
+ // Write the main routes.d.ts file
241
+ await fs . promises . writeFile ( filePath , generateRouteTypesFile ( manifest ) )
242
+
243
+ // Write the link.d.ts file if typedRoutes is enabled
244
+ if ( config . experimental ?. typedRoutes === true ) {
245
+ const linkTypesPath = path . join ( dirname , 'link.d.ts' )
246
+ await fs . promises . writeFile ( linkTypesPath , generateLinkTypesFile ( manifest ) )
247
+ }
216
248
}
0 commit comments