Skip to content

Commit 1e471f7

Browse files
committed
Fork validator.ts generation
1 parent a48bc79 commit 1e471f7

File tree

2 files changed

+250
-2
lines changed

2 files changed

+250
-2
lines changed

packages/next/src/server/lib/router-utils/route-types-utils.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
generateRouteTypesFile,
1111
generateLinkTypesFile,
1212
generateValidatorFile,
13+
generateValidatorFileStrict,
1314
} from './typegen'
1415
import { tryToParsePath } from '../../../lib/try-to-parse-path'
1516
import {
@@ -372,13 +373,19 @@ export async function writeRouteTypesManifest(
372373

373374
export async function writeValidatorFile(
374375
manifest: RouteTypesManifest,
375-
filePath: string
376+
filePath: string,
377+
strict: boolean
376378
) {
377379
const dirname = path.dirname(filePath)
378380

379381
if (!fs.existsSync(dirname)) {
380382
await fs.promises.mkdir(dirname, { recursive: true })
381383
}
382384

383-
await fs.promises.writeFile(filePath, generateValidatorFile(manifest))
385+
await fs.promises.writeFile(
386+
filePath,
387+
strict
388+
? generateValidatorFile(manifest)
389+
: generateValidatorFileStrict(manifest)
390+
)
384391
}

packages/next/src/server/lib/router-utils/typegen.ts

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/\.tsx?$/, '.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+
652893
export function generateRouteTypesFile(
653894
routesManifest: RouteTypesManifest
654895
): string {

0 commit comments

Comments
 (0)