next
Version:
The React Framework
255 lines (254 loc) • 10.9 kB
JavaScript
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