UNPKG

next

Version:

The React Framework

255 lines (254 loc) • 10.9 kB
import path from 'path'; import { getRouteRegex } from '../../../shared/lib/router/utils/route-regex'; import fs from 'fs'; import { generateRouteTypesFile, generateLinkTypesFile, generateValidatorFile } from './typegen'; import { tryToParsePath } from '../../../lib/try-to-parse-path'; import { extractInterceptionRouteInformation, isInterceptionRouteAppPath } from '../../../shared/lib/router/utils/interception-routes'; import { UNDERSCORE_GLOBAL_ERROR_ROUTE, UNDERSCORE_NOT_FOUND_ROUTE } from '../../../shared/lib/entry-constants'; import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep'; // Convert a custom-route source string (`/blog/:slug`, `/docs/:path*`, ...) // into the bracket-syntax used by other Next.js route helpers so that we can // reuse `getRouteRegex()` to extract groups. export function convertCustomRouteSource(source) { const parseResult = tryToParsePath(source); if (parseResult.error || !parseResult.tokens) { // Fallback to original source if parsing fails return source.startsWith('/') ? [ source ] : [ '/' + source ]; } const possibleNormalizedRoutes = [ '' ]; let slugCnt = 1; function append(suffix) { for(let i = 0; i < possibleNormalizedRoutes.length; i++){ possibleNormalizedRoutes[i] += suffix; } } function fork(suffix) { const currentLength = possibleNormalizedRoutes.length; for(let i = 0; i < currentLength; i++){ possibleNormalizedRoutes.push(possibleNormalizedRoutes[i] + suffix); } } for (const token of parseResult.tokens){ if (typeof token === 'object') { // Make sure the slug is always named. const slug = token.name || (slugCnt++ === 1 ? 'slug' : `slug${slugCnt}`); if (token.modifier === '*') { append(`${token.prefix}[[...${slug}]]`); } else if (token.modifier === '+') { append(`${token.prefix}[...${slug}]`); } else if (token.modifier === '') { if (token.pattern === '[^\\/#\\?]+?') { // A safe slug append(`${token.prefix}[${slug}]`); } else if (token.pattern === '.*') { // An optional catch-all slug append(`${token.prefix}[[...${slug}]]`); } else if (token.pattern === '.+') { // A catch-all slug append(`${token.prefix}[...${slug}]`); } else { // Other regex patterns are not supported. Skip this route. return []; } } else if (token.modifier === '?') { if (/^[a-zA-Z0-9_/]*$/.test(token.pattern)) { // An optional slug with plain text only, fork the route. append(token.prefix); fork(token.pattern); } else { // Optional modifier `?` and regex patterns are not supported. return []; } } } else if (typeof token === 'string') { append(token); } } // Ensure leading slash return possibleNormalizedRoutes.map((route)=>route.startsWith('/') ? route : '/' + route); } /** * Extracts route parameters from a route pattern */ export function extractRouteParams(route) { const regex = getRouteRegex(route); return regex.groups; } /** * Resolves an intercepting route to its canonical equivalent * Example: /gallery/test/(..)photo/[id] -> /gallery/photo/[id] */ function resolveInterceptingRoute(route) { // Reuse centralized interception route normalization logic try { if (!isInterceptionRouteAppPath(route)) return route; const { interceptedRoute } = extractInterceptionRouteInformation(route); return interceptedRoute; } catch { // If parsing fails, fall back to the original route return route; } } /** * Creates a route types manifest from processed route data * (used for both build and dev) */ export async function createRouteTypesManifest({ dir, pageRoutes, appRoutes, appRouteHandlers, pageApiRoutes, layoutRoutes, slots, redirects, rewrites, validatorFilePath }) { // Helper function to calculate the correct relative path const getRelativePath = (filePath)=>{ if (validatorFilePath) { // For validator generation, calculate path relative to validator directory return normalizePathSep(path.relative(path.dirname(validatorFilePath), filePath)); } // For other uses, calculate path relative to project directory return normalizePathSep(path.relative(dir, filePath)); }; const manifest = { appRoutes: {}, pageRoutes: {}, layoutRoutes: {}, appRouteHandlerRoutes: {}, redirectRoutes: {}, rewriteRoutes: {}, appRouteHandlers: new Set(appRouteHandlers.map(({ filePath })=>getRelativePath(filePath))), pageApiRoutes: new Set(pageApiRoutes.map(({ filePath })=>getRelativePath(filePath))), appPagePaths: new Set(appRoutes.map(({ filePath })=>getRelativePath(filePath))), pagesRouterPagePaths: new Set(pageRoutes.map(({ filePath })=>getRelativePath(filePath))), layoutPaths: new Set(layoutRoutes.map(({ filePath })=>getRelativePath(filePath))), filePathToRoute: new Map([ ...appRoutes.map(({ route, filePath })=>[ getRelativePath(filePath), resolveInterceptingRoute(route) ]), ...layoutRoutes.map(({ route, filePath })=>[ getRelativePath(filePath), resolveInterceptingRoute(route) ]), ...appRouteHandlers.map(({ route, filePath })=>[ getRelativePath(filePath), resolveInterceptingRoute(route) ]), ...pageRoutes.map(({ route, filePath })=>[ getRelativePath(filePath), route ]), ...pageApiRoutes.map(({ route, filePath })=>[ getRelativePath(filePath), route ]) ]) }; // Process page routes for (const { route, filePath } of pageRoutes){ manifest.pageRoutes[route] = { path: getRelativePath(filePath), groups: extractRouteParams(route) }; } // Process layout routes (exclude internal app error/not-found layouts) for (const { route, filePath } of layoutRoutes){ if (route === UNDERSCORE_GLOBAL_ERROR_ROUTE || route === UNDERSCORE_NOT_FOUND_ROUTE) continue; // Use the resolved route (for interception routes, this gives us the canonical route) const resolvedRoute = resolveInterceptingRoute(route); if (!manifest.layoutRoutes[resolvedRoute]) { manifest.layoutRoutes[resolvedRoute] = { path: getRelativePath(filePath), groups: extractRouteParams(resolvedRoute), slots: [] }; } } // Process slots for (const slot of slots){ if (manifest.layoutRoutes[slot.parent]) { manifest.layoutRoutes[slot.parent].slots.push(slot.name); } } // Process app routes (exclude internal app routes) for (const { route, filePath } of appRoutes){ if (route === UNDERSCORE_GLOBAL_ERROR_ROUTE || route === UNDERSCORE_NOT_FOUND_ROUTE) continue; // Don't include metadata routes or pages if (!filePath.endsWith('page.ts') && !filePath.endsWith('page.tsx') && !filePath.endsWith('.mdx') && !filePath.endsWith('.md')) { continue; } // Use the resolved route (for interception routes, this gives us the canonical route) const resolvedRoute = resolveInterceptingRoute(route); if (!manifest.appRoutes[resolvedRoute]) { manifest.appRoutes[resolvedRoute] = { path: getRelativePath(filePath), groups: extractRouteParams(resolvedRoute) }; } } // Process app route handlers for (const { route, filePath } of appRouteHandlers){ // Use the resolved route (for interception routes, this gives us the canonical route) const resolvedRoute = resolveInterceptingRoute(route); if (!manifest.appRouteHandlerRoutes[resolvedRoute]) { manifest.appRouteHandlerRoutes[resolvedRoute] = { path: getRelativePath(filePath), groups: extractRouteParams(resolvedRoute) }; } } // Process redirects if (typeof redirects === 'function') { const rd = await redirects(); for (const item of rd){ const possibleRoutes = convertCustomRouteSource(item.source); for (const route of possibleRoutes){ manifest.redirectRoutes[route] = { path: route, groups: extractRouteParams(route) }; } } } // Process rewrites if (typeof rewrites === 'function') { const rw = await rewrites(); const allSources = Array.isArray(rw) ? rw : [ ...(rw == null ? void 0 : rw.beforeFiles) || [], ...(rw == null ? void 0 : rw.afterFiles) || [], ...(rw == null ? void 0 : rw.fallback) || [] ]; for (const item of allSources){ const possibleRoutes = convertCustomRouteSource(item.source); for (const route of possibleRoutes){ manifest.rewriteRoutes[route] = { path: route, groups: extractRouteParams(route) }; } } } return manifest; } export async function writeRouteTypesManifest(manifest, filePath, config) { const dirname = path.dirname(filePath); if (!fs.existsSync(dirname)) { await fs.promises.mkdir(dirname, { recursive: true }); } // Write the main routes.d.ts file await fs.promises.writeFile(filePath, generateRouteTypesFile(manifest)); // Write the link.d.ts file if typedRoutes is enabled if (config.typedRoutes === true) { const linkTypesPath = path.join(dirname, 'link.d.ts'); await fs.promises.writeFile(linkTypesPath, generateLinkTypesFile(manifest)); } } export async function writeValidatorFile(manifest, filePath) { const dirname = path.dirname(filePath); if (!fs.existsSync(dirname)) { await fs.promises.mkdir(dirname, { recursive: true }); } await fs.promises.writeFile(filePath, generateValidatorFile(manifest)); } //# sourceMappingURL=route-types-utils.js.map