@@ -3,21 +3,23 @@ import * as path from 'node:path'
3
3
import * as fs from 'node:fs/promises'
4
4
import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths'
5
5
import { ensureLeadingSlash } from '../../../shared/lib/page-path/ensure-leading-slash'
6
+ import type { DynamicParamTypes } from '../../../server/app-render/types'
6
7
import { getSegmentParam } from '../../../server/app-render/get-segment-param'
8
+ import { InvariantError } from '../../../shared/lib/invariant-error'
7
9
8
10
export type RootParamsLoaderOpts = {
9
11
appDir : string
10
12
pageExtensions : string [ ]
11
13
}
12
14
13
- type CollectedRootParams = Set < string >
15
+ export type CollectedRootParams = Map < string , Set < DynamicParamTypes > >
14
16
15
17
const rootParamsLoader : webpack . LoaderDefinitionFunction < RootParamsLoaderOpts > =
16
18
async function ( ) {
17
19
const { appDir, pageExtensions } = this . getOptions ( )
18
20
19
21
const allRootParams = await collectRootParamsFromFileSystem ( {
20
- appDir,
22
+ appDir : appDir ,
21
23
pageExtensions,
22
24
} )
23
25
// invalidate the result whenever a file/directory is added/removed inside the app dir or its subdirectories,
@@ -30,7 +32,7 @@ const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
30
32
}
31
33
32
34
// Generate a getter for each root param we found.
33
- const sortedRootParamNames = Array . from ( allRootParams ) . sort ( )
35
+ const sortedRootParamNames = Array . from ( allRootParams . keys ( ) ) . sort ( )
34
36
const content = [
35
37
`import { getRootParam } from 'next/dist/server/request/root-params';` ,
36
38
...sortedRootParamNames . map ( ( paramName ) => {
@@ -43,7 +45,7 @@ const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
43
45
44
46
export default rootParamsLoader
45
47
46
- async function collectRootParamsFromFileSystem (
48
+ export async function collectRootParamsFromFileSystem (
47
49
opts : Parameters < typeof findRootLayouts > [ 0 ]
48
50
) {
49
51
return collectRootParams ( {
@@ -59,15 +61,22 @@ function collectRootParams({
59
61
rootLayoutFilePaths : string [ ]
60
62
appDir : string
61
63
} ) : CollectedRootParams {
62
- const allRootParams : CollectedRootParams = new Set ( )
64
+ // Collect the param names and kinds from all root layouts.
65
+ // Note that if multiple root layouts use the same param name, it can have multiple kinds.
66
+ const allRootParams : CollectedRootParams = new Map ( )
63
67
64
68
for ( const rootLayoutFilePath of rootLayoutFilePaths ) {
65
69
const params = getParamsFromLayoutFilePath ( {
66
70
appDir,
67
71
layoutFilePath : rootLayoutFilePath ,
68
72
} )
69
73
for ( const param of params ) {
70
- allRootParams . add ( param )
74
+ const { param : paramName , type : paramKind } = param
75
+ let paramKinds = allRootParams . get ( paramName )
76
+ if ( ! paramKinds ) {
77
+ allRootParams . set ( paramName , ( paramKinds = new Set ( ) ) )
78
+ }
79
+ paramKinds . add ( paramKind )
71
80
}
72
81
}
73
82
@@ -144,23 +153,84 @@ async function findRootLayouts({
144
153
return visit ( appDir )
145
154
}
146
155
156
+ type ParamInfo = { param : string ; type : DynamicParamTypes }
157
+
147
158
function getParamsFromLayoutFilePath ( {
148
159
appDir,
149
160
layoutFilePath,
150
161
} : {
151
162
appDir : string
152
163
layoutFilePath : string
153
- } ) : string [ ] {
164
+ } ) : ParamInfo [ ] {
154
165
const rootLayoutPath = normalizeAppPath (
155
166
ensureLeadingSlash ( path . dirname ( path . relative ( appDir , layoutFilePath ) ) )
156
167
)
157
168
const segments = rootLayoutPath . split ( '/' )
158
- const paramNames : string [ ] = [ ]
169
+ const params : ParamInfo [ ] = [ ]
159
170
for ( const segment of segments ) {
160
171
const param = getSegmentParam ( segment )
161
172
if ( param !== null ) {
162
- paramNames . push ( param . param )
173
+ params . push ( param )
174
+ }
175
+ }
176
+ return params
177
+ }
178
+
179
+ //=============================================
180
+ // Type declarations
181
+ //=============================================
182
+
183
+ export function generateDeclarations ( rootParams : CollectedRootParams ) {
184
+ const sortedRootParamNames = Array . from ( rootParams . keys ( ) ) . sort ( )
185
+ const declarationLines = sortedRootParamNames
186
+ . map ( ( paramName ) => {
187
+ // A param can have multiple kinds (in different root layouts).
188
+ // In that case, we'll need to union the types together together.
189
+ const paramKinds = Array . from ( rootParams . get ( paramName ) ! )
190
+ const possibleTypesForParam = paramKinds . map ( ( kind ) =>
191
+ getTypescriptTypeFromParamKind ( kind )
192
+ )
193
+ // A root param getter can be called
194
+ // - in a route handler (not yet implemented)
195
+ // - a server action (unsupported)
196
+ // - in another root layout that doesn't share the same root params.
197
+ // For this reason, we currently always want `... | undefined` in the type.
198
+ possibleTypesForParam . push ( `undefined` )
199
+
200
+ const paramType = unionTsTypes ( possibleTypesForParam )
201
+
202
+ return [
203
+ ` /** Allows reading the '${ paramName } ' root param. */` ,
204
+ ` export function ${ paramName } (): Promise<${ paramType } >` ,
205
+ ] . join ( '\n' )
206
+ } )
207
+ . join ( '\n\n' )
208
+
209
+ return `declare module 'next/root-params' {\n${ declarationLines } \n}\n`
210
+ }
211
+
212
+ function getTypescriptTypeFromParamKind ( kind : DynamicParamTypes ) : string {
213
+ switch ( kind ) {
214
+ case 'catchall' :
215
+ case 'catchall-intercepted' : {
216
+ return `string[]`
217
+ }
218
+ case 'optional-catchall' : {
219
+ return `string[] | undefined`
220
+ }
221
+ case 'dynamic' :
222
+ case 'dynamic-intercepted' : {
223
+ return `string`
224
+ }
225
+ default : {
226
+ kind satisfies never
227
+ throw new InvariantError ( `Unknown param kind ${ kind } ` )
163
228
}
164
229
}
165
- return paramNames
230
+ }
231
+
232
+ function unionTsTypes ( types : string [ ] ) {
233
+ if ( types . length === 0 ) return 'never'
234
+ if ( types . length === 1 ) return types [ 0 ]
235
+ return types . map ( ( type ) => `(${ type } )` ) . join ( ' | ' )
166
236
}
0 commit comments