@@ -649,6 +649,247 @@ ${layoutValidations}
649649`
650650}
651651
652+ export function generateValidatorFileStrict (
653+ routesManifest : RouteTypesManifest
654+ ) : string {
655+ const generateValidations = (
656+ paths : string [ ] ,
657+ type :
658+ | 'AppPageConfig'
659+ | 'PagesPageConfig'
660+ | 'LayoutConfig'
661+ | 'RouteHandlerConfig'
662+ | 'ApiRouteConfig' ,
663+ pathToRouteMap ?: Map < string , string >
664+ ) =>
665+ paths
666+ . sort ( )
667+ // Only validate TypeScript files - JavaScript files have too many type inference limitations
668+ . filter (
669+ ( filePath ) => filePath . endsWith ( '.ts' ) || filePath . endsWith ( '.tsx' )
670+ )
671+ . filter (
672+ // Don't include metadata routes or pages
673+ // (e.g. /manifest.webmanifest)
674+ ( filePath ) =>
675+ type !== 'AppPageConfig' ||
676+ filePath . endsWith ( 'page.ts' ) ||
677+ filePath . endsWith ( 'page.tsx' )
678+ )
679+ . map ( ( filePath ) => {
680+ // Keep the file extension for TypeScript imports to support node16 module resolution
681+ const importPath = filePath
682+ const route = pathToRouteMap ?. get ( filePath )
683+ const typeWithRoute =
684+ route &&
685+ ( type === 'AppPageConfig' ||
686+ type === 'LayoutConfig' ||
687+ type === 'RouteHandlerConfig' )
688+ ? `${ type } <${ JSON . stringify ( route ) } >`
689+ : type
690+
691+ // NOTE: we previously used `satisfies` here, but it's not supported by TypeScript 4.8 and below.
692+ // If we ever raise the TS minimum version, we can switch back.
693+
694+ return `// Validate ${ filePath }
695+ {
696+ type __IsExpected<Specific extends ${ typeWithRoute } > = Specific
697+ const handler = {} as typeof import(${ JSON . stringify (
698+ importPath . replace ( / \. t s x ? $ / , '.js' )
699+ ) } )
700+ type __Check = __IsExpected<typeof handler>
701+ // @ts-ignore
702+ type __Unused = __Check
703+ }`
704+ } )
705+ . join ( '\n\n' )
706+
707+ // Use direct mappings from the manifest
708+
709+ // Generate validations for different route types
710+ const appPageValidations = generateValidations (
711+ Array . from ( routesManifest . appPagePaths ) . sort ( ) ,
712+ 'AppPageConfig' ,
713+ routesManifest . filePathToRoute
714+ )
715+ const appRouteHandlerValidations = generateValidations (
716+ Array . from ( routesManifest . appRouteHandlers ) . sort ( ) ,
717+ 'RouteHandlerConfig' ,
718+ routesManifest . filePathToRoute
719+ )
720+ const pagesRouterPageValidations = generateValidations (
721+ Array . from ( routesManifest . pagesRouterPagePaths ) . sort ( ) ,
722+ 'PagesPageConfig'
723+ )
724+ const pagesApiRouteValidations = generateValidations (
725+ Array . from ( routesManifest . pageApiRoutes ) . sort ( ) ,
726+ 'ApiRouteConfig'
727+ )
728+ const layoutValidations = generateValidations (
729+ Array . from ( routesManifest . layoutPaths ) . sort ( ) ,
730+ 'LayoutConfig' ,
731+ routesManifest . filePathToRoute
732+ )
733+
734+ const hasAppRouteHandlers =
735+ Object . keys ( routesManifest . appRouteHandlerRoutes ) . length > 0
736+
737+ // Build type definitions based on what's actually used
738+ let typeDefinitions = ''
739+
740+ if ( appPageValidations ) {
741+ typeDefinitions += `type AppPageConfig<Route extends AppRoutes = AppRoutes> = {
742+ default: React.ComponentType<{ params: Promise<ParamMap[Route]> } & any> | ((props: { params: Promise<ParamMap[Route]> } & any) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
743+ generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
744+ generateMetadata?: (
745+ props: { params: Promise<ParamMap[Route]> } & any,
746+ parent: ResolvingMetadata
747+ ) => Promise<any> | any
748+ generateViewport?: (
749+ props: { params: Promise<ParamMap[Route]> } & any,
750+ parent: ResolvingViewport
751+ ) => Promise<any> | any
752+ metadata?: any
753+ viewport?: any
754+ }
755+
756+ `
757+ }
758+
759+ if ( pagesRouterPageValidations ) {
760+ typeDefinitions += `type PagesPageConfig = {
761+ default: React.ComponentType<any> | ((props: any) => React.ReactNode | Promise<React.ReactNode> | never | void)
762+ getStaticProps?: (context: any) => Promise<any> | any
763+ getStaticPaths?: (context: any) => Promise<any> | any
764+ getServerSideProps?: (context: any) => Promise<any> | any
765+ getInitialProps?: (context: any) => Promise<any> | any
766+ /**
767+ * Segment configuration for legacy Pages Router pages.
768+ * Validated at build-time by parsePagesSegmentConfig.
769+ */
770+ config?: {
771+ maxDuration?: number
772+ runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const
773+ regions?: string[]
774+ }
775+ }
776+
777+ `
778+ }
779+
780+ if ( layoutValidations ) {
781+ typeDefinitions += `type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
782+ default: React.ComponentType<LayoutProps<Route>> | ((props: LayoutProps<Route>) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
783+ generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
784+ generateMetadata?: (
785+ props: { params: Promise<ParamMap[Route]> } & any,
786+ parent: ResolvingMetadata
787+ ) => Promise<any> | any
788+ generateViewport?: (
789+ props: { params: Promise<ParamMap[Route]> } & any,
790+ parent: ResolvingViewport
791+ ) => Promise<any> | any
792+ metadata?: any
793+ viewport?: any
794+ }
795+
796+ `
797+ }
798+
799+ if ( appRouteHandlerValidations ) {
800+ typeDefinitions += `type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRoutes> = {
801+ GET?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
802+ POST?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
803+ PUT?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
804+ PATCH?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
805+ DELETE?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
806+ HEAD?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
807+ OPTIONS?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
808+ }
809+
810+ `
811+ }
812+
813+ if ( pagesApiRouteValidations ) {
814+ typeDefinitions += `type ApiRouteConfig = {
815+ default: (req: any, res: any) => ReturnType<NextApiHandler>
816+ config?: {
817+ api?: {
818+ bodyParser?: boolean | { sizeLimit?: string }
819+ responseLimit?: string | number | boolean
820+ externalResolver?: boolean
821+ }
822+ runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const
823+ maxDuration?: number
824+ }
825+ }
826+
827+ `
828+ }
829+
830+ // Build import statement based on what's actually needed
831+ const routeImports = [ ]
832+
833+ // Only import AppRoutes if there are app pages
834+ if ( appPageValidations ) {
835+ routeImports . push ( 'AppRoutes' )
836+ }
837+
838+ // Only import LayoutRoutes if there are layouts
839+ if ( layoutValidations ) {
840+ routeImports . push ( 'LayoutRoutes' )
841+ }
842+
843+ // Only import ParamMap if there are routes that use it
844+ if ( appPageValidations || layoutValidations || appRouteHandlerValidations ) {
845+ routeImports . push ( 'ParamMap' )
846+ }
847+
848+ if ( hasAppRouteHandlers ) {
849+ routeImports . push ( 'AppRouteHandlerRoutes' )
850+ }
851+
852+ const routeImportStatement =
853+ routeImports . length > 0
854+ ? `import type { ${ routeImports . join ( ', ' ) } } from "./routes.js"`
855+ : ''
856+
857+ const nextRequestImport = hasAppRouteHandlers
858+ ? "import type { NextRequest } from 'next/server.js'\n"
859+ : ''
860+
861+ // Conditionally import types from next/types, merged into a single statement
862+ const nextTypes : string [ ] = [ ]
863+ if ( pagesApiRouteValidations ) {
864+ nextTypes . push ( 'NextApiHandler' )
865+ }
866+ if ( appPageValidations || layoutValidations ) {
867+ nextTypes . push ( 'ResolvingMetadata' , 'ResolvingViewport' )
868+ }
869+ const nextTypesImport =
870+ nextTypes . length > 0
871+ ? `import type { ${ nextTypes . join ( ', ' ) } } from "next/types.js"\n`
872+ : ''
873+
874+ return `// This file is generated automatically by Next.js
875+ // Do not edit this file manually
876+ // This file validates that all pages and layouts export the correct types
877+
878+ ${ routeImportStatement }
879+ ${ nextTypesImport } ${ nextRequestImport }
880+ ${ typeDefinitions }
881+ ${ appPageValidations }
882+
883+ ${ appRouteHandlerValidations }
884+
885+ ${ pagesRouterPageValidations }
886+
887+ ${ pagesApiRouteValidations }
888+
889+ ${ layoutValidations }
890+ `
891+ }
892+
652893export function generateRouteTypesFile (
653894 routesManifest : RouteTypesManifest
654895) : string {
0 commit comments