@@ -237,6 +237,7 @@ async function collectNamedSlots(layoutPath: string) {
237237// possible to provide the same experience for dynamic routes.
238238
239239const pluginState = getProxiedPluginState ( {
240+ collectedRootParams : { } as Record < string , string [ ] > ,
240241 routeTypes : {
241242 edge : {
242243 static : '' ,
@@ -584,6 +585,103 @@ function formatTimespanWithSeconds(seconds: undefined | number): string {
584585 return text + ' (' + descriptive + ')'
585586}
586587
588+ function getRootParamsFromLayouts ( layouts : Record < string , string [ ] > ) {
589+ // Sort layouts by depth (descending)
590+ const sortedLayouts = Object . entries ( layouts ) . sort (
591+ ( a , b ) => b [ 0 ] . split ( '/' ) . length - a [ 0 ] . split ( '/' ) . length
592+ )
593+
594+ if ( ! sortedLayouts . length ) {
595+ return [ ]
596+ }
597+
598+ // we assume the shorted layout path is the root layout
599+ let rootLayout = sortedLayouts [ sortedLayouts . length - 1 ] [ 0 ]
600+
601+ let rootParams = new Set < string > ( )
602+ let isMultipleRootLayouts = false
603+
604+ for ( const [ layoutPath , params ] of sortedLayouts ) {
605+ const allSegmentsAreDynamic = layoutPath
606+ . split ( '/' )
607+ . slice ( 1 , - 1 )
608+ // match dynamic params but not catch-all or optional catch-all
609+ . every ( ( segment ) => / ^ \[ [ ^ [ . \] ] + \] $ / . test ( segment ) )
610+
611+ if ( allSegmentsAreDynamic ) {
612+ if ( isSubpath ( rootLayout , layoutPath ) ) {
613+ // Current path is a subpath of the root layout, update root
614+ rootLayout = layoutPath
615+ rootParams = new Set ( params )
616+ } else {
617+ // Found another potential root layout
618+ isMultipleRootLayouts = true
619+ // Add any new params
620+ for ( const param of params ) {
621+ rootParams . add ( param )
622+ }
623+ }
624+ }
625+ }
626+
627+ // Create result array
628+ const result = Array . from ( rootParams ) . map ( ( param ) => ( {
629+ param,
630+ optional : isMultipleRootLayouts ,
631+ } ) )
632+
633+ return result
634+ }
635+
636+ function isSubpath ( parentLayoutPath : string , potentialChildLayoutPath : string ) {
637+ // we strip off the `layout` part of the path as those will always conflict with being a subpath
638+ const parentSegments = parentLayoutPath . split ( '/' ) . slice ( 1 , - 1 )
639+ const childSegments = potentialChildLayoutPath . split ( '/' ) . slice ( 1 , - 1 )
640+
641+ // child segments should be shorter or equal to parent segments to be a subpath
642+ if ( childSegments . length > parentSegments . length || ! childSegments . length )
643+ return false
644+
645+ // Verify all segment values are equal
646+ return childSegments . every (
647+ ( childSegment , index ) => childSegment === parentSegments [ index ]
648+ )
649+ }
650+
651+ function createServerDefinitions (
652+ rootParams : { param : string ; optional : boolean } [ ]
653+ ) {
654+ return `
655+ declare module 'next/server' {
656+
657+ import type { AsyncLocalStorage as NodeAsyncLocalStorage } from 'async_hooks'
658+ declare global {
659+ var AsyncLocalStorage: typeof NodeAsyncLocalStorage
660+ }
661+ export { NextFetchEvent } from 'next/dist/server/web/spec-extension/fetch-event'
662+ export { NextRequest } from 'next/dist/server/web/spec-extension/request'
663+ export { NextResponse } from 'next/dist/server/web/spec-extension/response'
664+ export { NextMiddleware, MiddlewareConfig } from 'next/dist/server/web/types'
665+ export { userAgentFromString } from 'next/dist/server/web/spec-extension/user-agent'
666+ export { userAgent } from 'next/dist/server/web/spec-extension/user-agent'
667+ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
668+ export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
669+ export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
670+ export { unstable_after } from 'next/dist/server/after'
671+ export { connection } from 'next/dist/server/request/connection'
672+ export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
673+ export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'
674+ export function unstable_rootParams(): Promise<{ ${ rootParams
675+ . map (
676+ ( { param, optional } ) =>
677+ // ensure params with dashes are valid keys
678+ `${ param . includes ( '-' ) ? `'${ param } '` : param } ${ optional ? '?' : '' } : string`
679+ )
680+ . join ( ', ' ) } }>
681+ }
682+ `
683+ }
684+
587685function createCustomCacheLifeDefinitions ( cacheLife : {
588686 [ profile : string ] : CacheLife
589687} ) {
@@ -855,6 +953,22 @@ export class NextTypesPlugin {
855953 if ( ! IS_IMPORTABLE ) return
856954
857955 if ( IS_LAYOUT ) {
956+ const rootLayoutPath = normalizeAppPath (
957+ ensureLeadingSlash (
958+ getPageFromPath (
959+ path . relative ( this . appDir , mod . resource ) ,
960+ this . pageExtensions
961+ )
962+ )
963+ )
964+
965+ const foundParams = Array . from (
966+ rootLayoutPath . matchAll ( / \[ ( .* ?) \] / g) ,
967+ ( match ) => match [ 1 ]
968+ )
969+
970+ pluginState . collectedRootParams [ rootLayoutPath ] = foundParams
971+
858972 const slots = await collectNamedSlots ( mod . resource )
859973 assets [ assetPath ] = new sources . RawSource (
860974 createTypeGuardFile ( mod . resource , relativeImportPath , {
@@ -933,6 +1047,22 @@ export class NextTypesPlugin {
9331047
9341048 await Promise . all ( promises )
9351049
1050+ const rootParams = getRootParamsFromLayouts (
1051+ pluginState . collectedRootParams
1052+ )
1053+ // If we discovered rootParams, we'll override the `next/server` types
1054+ // since we're able to determine the root params at build time.
1055+ if ( rootParams . length > 0 ) {
1056+ const serverTypesPath = path . join (
1057+ assetDirRelative ,
1058+ 'types/server.d.ts'
1059+ )
1060+
1061+ assets [ serverTypesPath ] = new sources . RawSource (
1062+ createServerDefinitions ( rootParams )
1063+ ) as unknown as webpack . sources . RawSource
1064+ }
1065+
9361066 // Support `"moduleResolution": "Node16" | "NodeNext"` with `"type": "module"`
9371067
9381068 const packageJsonAssetPath = path . join (
0 commit comments