UNPKG

next

Version:

The React Framework

613 lines (569 loc) • 24.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { generateLinkTypesFile: null, generateRouteTypesFile: null, generateValidatorFile: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { generateLinkTypesFile: function() { return generateLinkTypesFile; }, generateRouteTypesFile: function() { return generateRouteTypesFile; }, generateValidatorFile: function() { return generateValidatorFile; } }); const _isdynamic = require("../../../shared/lib/router/utils/is-dynamic"); function generateRouteTypes(routesManifest) { const appRoutes = Object.keys(routesManifest.appRoutes).sort(); const pageRoutes = Object.keys(routesManifest.pageRoutes).sort(); const layoutRoutes = Object.keys(routesManifest.layoutRoutes).sort(); const redirectRoutes = Object.keys(routesManifest.redirectRoutes).sort(); const rewriteRoutes = Object.keys(routesManifest.rewriteRoutes).sort(); let result = ''; // Generate AppRoutes union type (pages only) if (appRoutes.length > 0) { result += `type AppRoutes = ${appRoutes.map((route)=>JSON.stringify(route)).join(' | ')}\n`; } else { result += 'type AppRoutes = never\n'; } // Generate AppRouteHandlerRoutes union type for route handlers const appRouteHandlerRoutes = Object.keys(routesManifest.appRouteHandlerRoutes).sort(); const hasAppRouteHandlers = appRouteHandlerRoutes.length > 0; if (hasAppRouteHandlers) { result += `type AppRouteHandlerRoutes = ${appRouteHandlerRoutes.map((route)=>JSON.stringify(route)).join(' | ')}\n`; } // Generate PageRoutes union type if (pageRoutes.length > 0) { result += `type PageRoutes = ${pageRoutes.map((route)=>JSON.stringify(route)).join(' | ')}\n`; } else { result += 'type PageRoutes = never\n'; } // Generate LayoutRoutes union type if (layoutRoutes.length > 0) { result += `type LayoutRoutes = ${layoutRoutes.map((route)=>JSON.stringify(route)).join(' | ')}\n`; } else { result += 'type LayoutRoutes = never\n'; } // Generate RedirectRoutes union type if (redirectRoutes.length > 0) { result += `type RedirectRoutes = ${redirectRoutes.map((route)=>JSON.stringify(route)).join(' | ')}\n`; } else { result += 'type RedirectRoutes = never\n'; } // Generate RewriteRoutes union type if (rewriteRoutes.length > 0) { result += `type RewriteRoutes = ${rewriteRoutes.map((route)=>JSON.stringify(route)).join(' | ')}\n`; } else { result += 'type RewriteRoutes = never\n'; } // Only include AppRouteHandlerRoutes in Routes union if there are actual route handlers const routeUnionParts = [ 'AppRoutes', 'PageRoutes', 'LayoutRoutes', 'RedirectRoutes', 'RewriteRoutes' ]; if (hasAppRouteHandlers) { routeUnionParts.push('AppRouteHandlerRoutes'); } result += `type Routes = ${routeUnionParts.join(' | ')}\n`; return result; } function generateParamTypes(routesManifest) { const allRoutes = { ...routesManifest.appRoutes, ...routesManifest.appRouteHandlerRoutes, ...routesManifest.pageRoutes, ...routesManifest.layoutRoutes, ...routesManifest.redirectRoutes, ...routesManifest.rewriteRoutes }; let paramTypes = 'interface ParamMap {\n'; // Sort routes deterministically for consistent output const sortedRoutes = Object.entries(allRoutes).sort(([a], [b])=>a.localeCompare(b)); for (const [route, routeInfo] of sortedRoutes){ const { groups } = routeInfo; // For static routes (no dynamic segments), we can produce an empty parameter map. if (!(0, _isdynamic.isDynamicRoute)(route) || Object.keys(groups ?? {}).length === 0) { paramTypes += ` ${JSON.stringify(route)}: {}\n`; continue; } let paramType = '{'; // Process each group based on its properties for (const [key, group] of Object.entries(groups)){ const escapedKey = JSON.stringify(key); if (group.repeat) { // Catch-all parameters if (group.optional) { paramType += ` ${escapedKey}?: string[];`; } else { paramType += ` ${escapedKey}: string[];`; } } else { // Regular parameters if (group.optional) { paramType += ` ${escapedKey}?: string;`; } else { paramType += ` ${escapedKey}: string;`; } } } paramType += ' }'; paramTypes += ` ${JSON.stringify(route)}: ${paramType}\n`; } paramTypes += '}\n'; return paramTypes; } function generateLayoutSlotMap(routesManifest) { let slotMap = 'interface LayoutSlotMap {\n'; // Sort routes deterministically for consistent output const sortedLayoutRoutes = Object.entries(routesManifest.layoutRoutes).sort(([a], [b])=>a.localeCompare(b)); for (const [route, routeInfo] of sortedLayoutRoutes){ if ('slots' in routeInfo) { const slots = routeInfo.slots.sort(); if (slots.length > 0) { slotMap += ` ${JSON.stringify(route)}: ${slots.map((slot)=>JSON.stringify(slot)).join(' | ')}\n`; } else { slotMap += ` ${JSON.stringify(route)}: never\n`; } } else { slotMap += ` ${JSON.stringify(route)}: never\n`; } } slotMap += '}\n'; return slotMap; } // Helper function to format routes to route types (matches the plugin logic exactly) function formatRouteToRouteType(route) { const isDynamic = (0, _isdynamic.isDynamicRoute)(route); if (isDynamic) { route = route.split('/').map((part)=>{ if (part.startsWith('[') && part.endsWith(']')) { if (part.startsWith('[...')) { // /[...slug] return `\${CatchAllSlug<T>}`; } else if (part.startsWith('[[...') && part.endsWith(']]')) { // /[[...slug]] return `\${OptionalCatchAllSlug<T>}`; } // /[slug] return `\${SafeSlug<T>}`; } return part; }).join('/'); } return { isDynamic, routeType: route }; } // Helper function to serialize route types (matches the plugin logic exactly) function serializeRouteTypes(routeTypes) { // route collection is not deterministic, this makes the output of the file deterministic return routeTypes.sort().map((route)=>`\n | \`${route}\``).join(''); } function generateLinkTypesFile(routesManifest) { // Generate serialized static and dynamic routes for the internal namespace // Build a unified set of routes across app/pages/redirect/rewrite as well as // app route handlers and Pages Router API routes. const allRoutesSet = new Set([ ...Object.keys(routesManifest.appRoutes), ...Object.keys(routesManifest.pageRoutes), ...Object.keys(routesManifest.redirectRoutes), ...Object.keys(routesManifest.rewriteRoutes), // Allow linking to App Route Handlers (e.g. `/logout/route.ts`) ...Object.keys(routesManifest.appRouteHandlerRoutes), // Allow linking to Pages Router API routes (e.g. `/api/*`) ...Array.from(routesManifest.pageApiRoutes) ]); const staticRouteTypes = []; const dynamicRouteTypes = []; // Process each route using the same logic as the plugin for (const route of allRoutesSet){ const { isDynamic, routeType } = formatRouteToRouteType(route); if (isDynamic) { dynamicRouteTypes.push(routeType); } else { staticRouteTypes.push(routeType); } } const serializedStaticRouteTypes = serializeRouteTypes(staticRouteTypes); const serializedDynamicRouteTypes = serializeRouteTypes(dynamicRouteTypes); // If both StaticRoutes and DynamicRoutes are empty, fallback to type 'string & {}'. const routeTypesFallback = !serializedStaticRouteTypes && !serializedDynamicRouteTypes ? 'string & {}' : ''; return `// This file is generated automatically by Next.js // Do not edit this file manually // Type definitions for Next.js routes /** * Internal types used by the Next.js router and Link component. * These types are not meant to be used directly. * @internal */ declare namespace __next_route_internal_types__ { type SearchOrHash = \`?\${string}\` | \`#\${string}\` type WithProtocol = \`\${string}:\${string}\` type Suffix = '' | SearchOrHash type SafeSlug<S extends string> = S extends \`\${string}/\${string}\` ? never : S extends \`\${string}\${SearchOrHash}\` ? never : S extends '' ? never : S type CatchAllSlug<S extends string> = S extends \`\${string}\${SearchOrHash}\` ? never : S extends '' ? never : S type OptionalCatchAllSlug<S extends string> = S extends \`\${string}\${SearchOrHash}\` ? never : S type StaticRoutes = ${serializedStaticRouteTypes || 'never'} type DynamicRoutes<T extends string = string> = ${serializedDynamicRouteTypes || 'never'} type RouteImpl<T> = ${routeTypesFallback || ` ${// This keeps autocompletion working for static routes. '| StaticRoutes'} | SearchOrHash | WithProtocol | \`\${StaticRoutes}\${SearchOrHash}\` | (T extends \`\${DynamicRoutes<infer _>}\${Suffix}\` ? T : never) `} } declare module 'next' { export { default } from 'next/types.js' export * from 'next/types.js' export type Route<T extends string = string> = __next_route_internal_types__.RouteImpl<T> } declare module 'next/link' { export { useLinkStatus } from 'next/dist/client/link.js' import type { LinkProps as OriginalLinkProps } from 'next/dist/client/link.js' import type { AnchorHTMLAttributes, DetailedHTMLProps } from 'react' import type { UrlObject } from 'url' type LinkRestProps = Omit< Omit< DetailedHTMLProps< AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement >, keyof OriginalLinkProps > & OriginalLinkProps, 'href' > export type LinkProps<RouteInferType> = LinkRestProps & { /** * The path or URL to navigate to. This is the only required prop. It can also be an object. * @see https://nextjs.org/docs/api-reference/next/link */ href: __next_route_internal_types__.RouteImpl<RouteInferType> | UrlObject } export default function Link<RouteType>(props: LinkProps<RouteType>): JSX.Element } declare module 'next/navigation' { export * from 'next/dist/client/components/navigation.js' import type { NavigateOptions, AppRouterInstance as OriginalAppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js' import type { RedirectType } from 'next/dist/client/components/redirect-error.js' interface AppRouterInstance extends OriginalAppRouterInstance { /** * Navigate to the provided href. * Pushes a new history entry. */ push<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void /** * Navigate to the provided href. * Replaces the current history entry. */ replace<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void /** * Prefetch the provided href. */ prefetch<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>): void } export function useRouter(): AppRouterInstance; /** * This function allows you to redirect the user to another URL. It can be used in * [Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components), * [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers), and * [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations). * * - In a Server Component, this will insert a meta tag to redirect the user to the target page. * - In a Route Handler or Server Action, it will serve a 307/303 to the caller. * - In a Server Action, type defaults to 'push' and 'replace' elsewhere. * * Read more: [Next.js Docs: redirect](https://nextjs.org/docs/app/api-reference/functions/redirect) */ export function redirect<RouteType>( /** The URL to redirect to */ url: __next_route_internal_types__.RouteImpl<RouteType>, type?: RedirectType ): never; /** * This function allows you to redirect the user to another URL. It can be used in * [Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components), * [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers), and * [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations). * * - In a Server Component, this will insert a meta tag to redirect the user to the target page. * - In a Route Handler or Server Action, it will serve a 308/303 to the caller. * * Read more: [Next.js Docs: redirect](https://nextjs.org/docs/app/api-reference/functions/redirect) */ export function permanentRedirect<RouteType>( /** The URL to redirect to */ url: __next_route_internal_types__.RouteImpl<RouteType>, type?: RedirectType ): never; } declare module 'next/form' { import type { FormProps as OriginalFormProps } from 'next/dist/client/form.js' type FormRestProps = Omit<OriginalFormProps, 'action'> export type FormProps<RouteInferType> = { /** * \`action\` can be either a \`string\` or a function. * - If \`action\` is a string, it will be interpreted as a path or URL to navigate to when the form is submitted. * The path will be prefetched when the form becomes visible. * - If \`action\` is a function, it will be called when the form is submitted. See the [React docs](https://react.dev/reference/react-dom/components/form#props) for more. */ action: __next_route_internal_types__.RouteImpl<RouteInferType> | ((formData: FormData) => void) } & FormRestProps export default function Form<RouteType>(props: FormProps<RouteType>): JSX.Element } `; } function generateValidatorFile(routesManifest) { const generateValidations = (paths, type, pathToRouteMap)=>paths.sort()// Only validate TypeScript files - JavaScript files have too many type inference limitations .filter((filePath)=>filePath.endsWith('.ts') || filePath.endsWith('.tsx')).filter(// Don't include metadata routes or pages // (e.g. /manifest.webmanifest) (filePath)=>type !== 'AppPageConfig' || filePath.endsWith('page.ts') || filePath.endsWith('page.tsx')).map((filePath)=>{ // Keep the file extension for TypeScript imports to support node16 module resolution const importPath = filePath; const route = pathToRouteMap == null ? void 0 : pathToRouteMap.get(filePath); const typeWithRoute = route && (type === 'AppPageConfig' || type === 'LayoutConfig' || type === 'RouteHandlerConfig') ? `${type}<${JSON.stringify(route)}>` : type; // NOTE: we previously used `satisfies` here, but it's not supported by TypeScript 4.8 and below. // If we ever raise the TS minimum version, we can switch back. return `// Validate ${filePath} { type __IsExpected<Specific extends ${typeWithRoute}> = Specific const handler = {} as typeof import(${JSON.stringify(importPath.replace(/\.tsx?$/, '.js'))}) type __Check = __IsExpected<typeof handler> // @ts-ignore type __Unused = __Check }`; }).join('\n\n'); // Use direct mappings from the manifest // Generate validations for different route types const appPageValidations = generateValidations(Array.from(routesManifest.appPagePaths).sort(), 'AppPageConfig', routesManifest.filePathToRoute); const appRouteHandlerValidations = generateValidations(Array.from(routesManifest.appRouteHandlers).sort(), 'RouteHandlerConfig', routesManifest.filePathToRoute); const pagesRouterPageValidations = generateValidations(Array.from(routesManifest.pagesRouterPagePaths).sort(), 'PagesPageConfig'); const pagesApiRouteValidations = generateValidations(Array.from(routesManifest.pageApiRoutes).sort(), 'ApiRouteConfig'); const layoutValidations = generateValidations(Array.from(routesManifest.layoutPaths).sort(), 'LayoutConfig', routesManifest.filePathToRoute); const hasAppRouteHandlers = Object.keys(routesManifest.appRouteHandlerRoutes).length > 0; // Build type definitions based on what's actually used let typeDefinitions = ''; if (appPageValidations) { typeDefinitions += `type AppPageConfig<Route extends AppRoutes = AppRoutes> = { default: React.ComponentType<{ params: Promise<ParamMap[Route]> } & any> | ((props: { params: Promise<ParamMap[Route]> } & any) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>) generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[] generateMetadata?: ( props: { params: Promise<ParamMap[Route]> } & any, parent: ResolvingMetadata ) => Promise<any> | any generateViewport?: ( props: { params: Promise<ParamMap[Route]> } & any, parent: ResolvingViewport ) => Promise<any> | any metadata?: any viewport?: any } `; } if (pagesRouterPageValidations) { typeDefinitions += `type PagesPageConfig = { default: React.ComponentType<any> | ((props: any) => React.ReactNode | Promise<React.ReactNode> | never | void) getStaticProps?: (context: any) => Promise<any> | any getStaticPaths?: (context: any) => Promise<any> | any getServerSideProps?: (context: any) => Promise<any> | any getInitialProps?: (context: any) => Promise<any> | any /** * Segment configuration for legacy Pages Router pages. * Validated at build-time by parsePagesSegmentConfig. */ config?: { maxDuration?: number runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const regions?: string[] } } `; } if (layoutValidations) { typeDefinitions += `type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = { default: React.ComponentType<LayoutProps<Route>> | ((props: LayoutProps<Route>) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>) generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[] generateMetadata?: ( props: { params: Promise<ParamMap[Route]> } & any, parent: ResolvingMetadata ) => Promise<any> | any generateViewport?: ( props: { params: Promise<ParamMap[Route]> } & any, parent: ResolvingViewport ) => Promise<any> | any metadata?: any viewport?: any } `; } if (appRouteHandlerValidations) { typeDefinitions += `type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRoutes> = { GET?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void POST?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void PUT?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void PATCH?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void DELETE?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void HEAD?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void OPTIONS?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void } `; } if (pagesApiRouteValidations) { typeDefinitions += `type ApiRouteConfig = { default: (req: any, res: any) => ReturnType<NextApiHandler> config?: { api?: { bodyParser?: boolean | { sizeLimit?: string } responseLimit?: string | number | boolean externalResolver?: boolean } runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const maxDuration?: number } } `; } // Build import statement based on what's actually needed const routeImports = []; // Only import AppRoutes if there are app pages if (appPageValidations) { routeImports.push('AppRoutes'); } // Only import LayoutRoutes if there are layouts if (layoutValidations) { routeImports.push('LayoutRoutes'); } // Only import ParamMap if there are routes that use it if (appPageValidations || layoutValidations || appRouteHandlerValidations) { routeImports.push('ParamMap'); } if (hasAppRouteHandlers) { routeImports.push('AppRouteHandlerRoutes'); } const routeImportStatement = routeImports.length > 0 ? `import type { ${routeImports.join(', ')} } from "./routes.js"` : ''; const nextRequestImport = hasAppRouteHandlers ? "import type { NextRequest } from 'next/server.js'\n" : ''; // Conditionally import types from next/types, merged into a single statement const nextTypes = []; if (pagesApiRouteValidations) { nextTypes.push('NextApiHandler'); } if (appPageValidations || layoutValidations) { nextTypes.push('ResolvingMetadata', 'ResolvingViewport'); } const nextTypesImport = nextTypes.length > 0 ? `import type { ${nextTypes.join(', ')} } from "next/types.js"\n` : ''; return `// This file is generated automatically by Next.js // Do not edit this file manually // This file validates that all pages and layouts export the correct types ${routeImportStatement} ${nextTypesImport}${nextRequestImport} ${typeDefinitions} ${appPageValidations} ${appRouteHandlerValidations} ${pagesRouterPageValidations} ${pagesApiRouteValidations} ${layoutValidations} `; } function generateRouteTypesFile(routesManifest) { const routeTypes = generateRouteTypes(routesManifest); const paramTypes = generateParamTypes(routesManifest); const layoutSlotMap = generateLayoutSlotMap(routesManifest); const hasAppRouteHandlers = Object.keys(routesManifest.appRouteHandlerRoutes).length > 0; // Build export statement based on what's actually generated const routeExports = [ 'AppRoutes', 'PageRoutes', 'LayoutRoutes', 'RedirectRoutes', 'RewriteRoutes', 'ParamMap' ]; if (hasAppRouteHandlers) { routeExports.push('AppRouteHandlerRoutes'); } const exportStatement = `export type { ${routeExports.join(', ')} }`; const routeContextInterface = hasAppRouteHandlers ? ` /** * Context for Next.js App Router route handlers * @example * \`\`\`tsx * export async function GET(request: NextRequest, context: RouteContext<'/api/users/[id]'>) { * const { id } = await context.params * return Response.json({ id }) * } * \`\`\` */ interface RouteContext<AppRouteHandlerRoute extends AppRouteHandlerRoutes> { params: Promise<ParamMap[AppRouteHandlerRoute]> }` : ''; return `// This file is generated automatically by Next.js // Do not edit this file manually ${routeTypes} ${paramTypes} export type ParamsOf<Route extends Routes> = ParamMap[Route] ${layoutSlotMap} ${exportStatement} declare global { /** * Props for Next.js App Router page components * @example * \`\`\`tsx * export default function Page(props: PageProps<'/blog/[slug]'>) { * const { slug } = await props.params * return <div>Blog post: {slug}</div> * } * \`\`\` */ interface PageProps<AppRoute extends AppRoutes> { params: Promise<ParamMap[AppRoute]> searchParams: Promise<Record<string, string | string[] | undefined>> } /** * Props for Next.js App Router layout components * @example * \`\`\`tsx * export default function Layout(props: LayoutProps<'/dashboard'>) { * return <div>{props.children}</div> * } * \`\`\` */ type LayoutProps<LayoutRoute extends LayoutRoutes> = { params: Promise<ParamMap[LayoutRoute]> children: React.ReactNode } & { [K in LayoutSlotMap[LayoutRoute]]: React.ReactNode }${routeContextInterface} } `; } //# sourceMappingURL=typegen.js.map