@@ -3,21 +3,23 @@ import * as path from 'node:path'
33import  *  as  fs  from  'node:fs/promises' 
44import  {  normalizeAppPath  }  from  '../../../shared/lib/router/utils/app-paths' 
55import  {  ensureLeadingSlash  }  from  '../../../shared/lib/page-path/ensure-leading-slash' 
6+ import  type  {  DynamicParamTypes  }  from  '../../../server/app-render/types' 
67import  {  getSegmentParam  }  from  '../../../server/app-render/get-segment-param' 
8+ import  {  InvariantError  }  from  '../../../shared/lib/invariant-error' 
79
810export  type  RootParamsLoaderOpts  =  { 
911  appDir : string 
1012  pageExtensions : string [ ] 
1113} 
1214
13- type  CollectedRootParams  =  Set < string > 
15+ export   type  CollectedRootParams  =  Map < string ,   Set < DynamicParamTypes > > 
1416
1517const  rootParamsLoader : webpack . LoaderDefinitionFunction < RootParamsLoaderOpts >  = 
1618  async  function  ( )  { 
1719    const  {  appDir,  pageExtensions }  =  this . getOptions ( ) 
1820
1921    const  allRootParams  =  await  collectRootParamsFromFileSystem ( { 
20-       appDir, 
22+       appDir :  appDir , 
2123      pageExtensions, 
2224      // Track every directory we traverse in case a layout gets added to it 
2325      // (which would make it the new root layout for that subtree). 
@@ -31,7 +33,7 @@ const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
3133    } 
3234
3335    // Generate a getter for each root param we found. 
34-     const  sortedRootParamNames  =  Array . from ( allRootParams ) . sort ( ) 
36+     const  sortedRootParamNames  =  Array . from ( allRootParams . keys ( ) ) . sort ( ) 
3537    const  content  =  [ 
3638      `import { getRootParam } from 'next/dist/server/request/root-params';` , 
3739      ...sortedRootParamNames . map ( ( paramName )  =>  { 
@@ -44,7 +46,7 @@ const rootParamsLoader: webpack.LoaderDefinitionFunction<RootParamsLoaderOpts> =
4446
4547export  default  rootParamsLoader 
4648
47- async  function  collectRootParamsFromFileSystem ( 
49+ export   async  function  collectRootParamsFromFileSystem ( 
4850  opts : Parameters < typeof  findRootLayouts > [ 0 ] 
4951)  { 
5052  return  collectRootParams ( { 
@@ -60,15 +62,22 @@ function collectRootParams({
6062  rootLayoutFilePaths : string [ ] 
6163  appDir : string 
6264} ) : CollectedRootParams  { 
63-   const  allRootParams : CollectedRootParams  =  new  Set ( ) 
65+   // Collect the param names and kinds from all root layouts. 
66+   // Note that if multiple root layouts use the same param name, it can have multiple kinds. 
67+   const  allRootParams : CollectedRootParams  =  new  Map ( ) 
6468
6569  for  ( const  rootLayoutFilePath  of  rootLayoutFilePaths )  { 
6670    const  params  =  getParamsFromLayoutFilePath ( { 
6771      appDir, 
6872      layoutFilePath : rootLayoutFilePath , 
6973    } ) 
7074    for  ( const  param  of  params )  { 
71-       allRootParams . add ( param ) 
75+       const  {  param : paramName ,  type : paramKind  }  =  param 
76+       let  paramKinds  =  allRootParams . get ( paramName ) 
77+       if  ( ! paramKinds )  { 
78+         allRootParams . set ( paramName ,  ( paramKinds  =  new  Set ( ) ) ) 
79+       } 
80+       paramKinds . add ( paramKind ) 
7281    } 
7382  } 
7483
@@ -149,23 +158,84 @@ async function findRootLayouts({
149158  return  visit ( appDir ) 
150159} 
151160
161+ type  ParamInfo  =  {  param : string ;  type : DynamicParamTypes  } 
162+ 
152163function  getParamsFromLayoutFilePath ( { 
153164  appDir, 
154165  layoutFilePath, 
155166} : { 
156167  appDir : string 
157168  layoutFilePath : string 
158- } ) : string [ ]  { 
169+ } ) : ParamInfo [ ]  { 
159170  const  rootLayoutPath  =  normalizeAppPath ( 
160171    ensureLeadingSlash ( path . dirname ( path . relative ( appDir ,  layoutFilePath ) ) ) 
161172  ) 
162173  const  segments  =  rootLayoutPath . split ( '/' ) 
163-   const  paramNames :  string [ ]  =  [ ] 
174+   const  params :  ParamInfo [ ]  =  [ ] 
164175  for  ( const  segment  of  segments )  { 
165176    const  param  =  getSegmentParam ( segment ) 
166177    if  ( param  !==  null )  { 
167-       paramNames . push ( param . param ) 
178+       params . push ( param ) 
179+     } 
180+   } 
181+   return  params 
182+ } 
183+ 
184+ //============================================= 
185+ // Type declarations 
186+ //============================================= 
187+ 
188+ export  function  generateDeclarations ( rootParams : CollectedRootParams )  { 
189+   const  sortedRootParamNames  =  Array . from ( rootParams . keys ( ) ) . sort ( ) 
190+   const  declarationLines  =  sortedRootParamNames 
191+     . map ( ( paramName )  =>  { 
192+       // A param can have multiple kinds (in different root layouts). 
193+       // In that case, we'll need to union the types together together. 
194+       const  paramKinds  =  Array . from ( rootParams . get ( paramName ) ! ) 
195+       const  possibleTypesForParam  =  paramKinds . map ( ( kind )  => 
196+         getTypescriptTypeFromParamKind ( kind ) 
197+       ) 
198+       // A root param getter can be called 
199+       // - in a route handler (not yet implemented) 
200+       // - a server action (unsupported) 
201+       // - in another root layout that doesn't share the same root params. 
202+       // For this reason, we currently always want `... | undefined` in the type. 
203+       possibleTypesForParam . push ( `undefined` ) 
204+ 
205+       const  paramType  =  unionTsTypes ( possibleTypesForParam ) 
206+ 
207+       return  [ 
208+         `  /** Allows reading the '${ paramName }  ' root param. */` , 
209+         `  export function ${ paramName }  (): Promise<${ paramType }  >` , 
210+       ] . join ( '\n' ) 
211+     } ) 
212+     . join ( '\n\n' ) 
213+ 
214+   return  `declare module 'next/root-params' {\n${ declarationLines }  \n}\n` 
215+ } 
216+ 
217+ function  getTypescriptTypeFromParamKind ( kind : DynamicParamTypes ) : string  { 
218+   switch  ( kind )  { 
219+     case  'catchall' :
220+     case  'catchall-intercepted' : { 
221+       return  `string[]` 
222+     } 
223+     case  'optional-catchall' : { 
224+       return  `string[] | undefined` 
225+     } 
226+     case  'dynamic' :
227+     case  'dynamic-intercepted' : { 
228+       return  `string` 
229+     } 
230+     default : { 
231+       kind  satisfies  never 
232+       throw  new  InvariantError ( `Unknown param kind ${ kind }  ` ) 
168233    } 
169234  } 
170-   return  paramNames 
235+ } 
236+ 
237+ function  unionTsTypes ( types : string [ ] )  { 
238+   if  ( types . length  ===  0 )  return  'never' 
239+   if  ( types . length  ===  1 )  return  types [ 0 ] 
240+   return  types . map ( ( type )  =>  `(${ type }  )` ) . join ( ' | ' ) 
171241} 
0 commit comments