UNPKG

next

Version:

The React Framework

242 lines (241 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "validateAppPaths", { enumerable: true, get: function() { return validateAppPaths; } }); const _getsegmentparam = require("../shared/lib/router/utils/get-segment-param"); const _app = require("../shared/lib/router/routes/app"); /** * Validates segment parameters for common syntax errors. * Based on validation logic from sorted-routes.ts */ function validateSegmentParam(param, pathname) { // Check for empty parameter names if (param.paramName.length === 0) { throw Object.defineProperty(new Error(`Parameter names cannot be empty in route "${pathname}".`), "__NEXT_ERROR_CODE", { value: "E922", enumerable: false, configurable: true }); } // Check for three-dot character (…) instead of ... if (param.paramName.includes('…')) { throw Object.defineProperty(new Error(`Detected a three-dot character ('…') in parameter "${param.paramName}" in route "${pathname}". Did you mean ('...')?`), "__NEXT_ERROR_CODE", { value: "E921", enumerable: false, configurable: true }); } // Check for optional non-catch-all segments (not yet supported) if (param.paramType !== 'optional-catchall' && param.paramName.startsWith('[') && param.paramName.endsWith(']')) { throw Object.defineProperty(new Error(`Optional route parameters are not yet supported ("[${param.paramName}]") in route "${pathname}".`), "__NEXT_ERROR_CODE", { value: "E926", enumerable: false, configurable: true }); } // Check for extra brackets if (param.paramName.startsWith('[') || param.paramName.endsWith(']')) { throw Object.defineProperty(new Error(`Segment names may not start or end with extra brackets ('${param.paramName}') in route "${pathname}".`), "__NEXT_ERROR_CODE", { value: "E916", enumerable: false, configurable: true }); } // Check for erroneous periods if (param.paramName.startsWith('.')) { throw Object.defineProperty(new Error(`Segment names may not start with erroneous periods ('${param.paramName}') in route "${pathname}".`), "__NEXT_ERROR_CODE", { value: "E920", enumerable: false, configurable: true }); } } /** * Validates a Route object for internal consistency. * Checks for duplicate slugs, invalid catch-all placement, and other route errors. * For interception routes, validates both the intercepting and intercepted routes separately. * Returns the validated segment parameters. */ function validateAppRoute(route) { // For interception routes, validate the intercepting and intercepted routes separately // This allows the same parameter name to appear in both parts if ((0, _app.isInterceptionAppRoute)(route)) { validateAppRoute(route.interceptingRoute); validateAppRoute(route.interceptedRoute); return; } // Then validate semantic constraints (duplicates, normalization, positioning) const slugNames = new Set(); const normalizedSegments = new Set(); let hasCatchAll = false; let hasOptionalCatchAllInPath = false; let catchAllPosition = -1; for(let i = 0; i < route.segments.length; i++){ const segment = route.segments[i]; // Type narrowing - only process dynamic segments if (segment.type === 'dynamic') { // First, validate syntax validateSegmentParam(segment.param, route.pathname); const properties = (0, _getsegmentparam.getParamProperties)(segment.param.paramType); if (properties.repeat) { if (properties.optional) { hasOptionalCatchAllInPath = true; } else { hasCatchAll = true; } catchAllPosition = i; } // Check to see if the parameter name is already in use. if (slugNames.has(segment.param.paramName)) { throw Object.defineProperty(new Error(`You cannot have the same slug name "${segment.param.paramName}" repeat within a single dynamic path in route "${route.pathname}".`), "__NEXT_ERROR_CODE", { value: "E914", enumerable: false, configurable: true }); } // Normalize parameter name for comparison by removing all non-word // characters. const normalizedSegment = segment.param.paramName.replace(/\W/g, ''); if (normalizedSegments.has(normalizedSegment)) { const existing = Array.from(slugNames).find((s)=>{ return s.replace(/\W/g, '') === normalizedSegment; }); throw Object.defineProperty(new Error(`You cannot have the slug names "${existing}" and "${segment.param.paramName}" differ only by non-word symbols within a single dynamic path in route "${route.pathname}".`), "__NEXT_ERROR_CODE", { value: "E919", enumerable: false, configurable: true }); } slugNames.add(segment.param.paramName); normalizedSegments.add(normalizedSegment); } // Check if catch-all is not at the end if (hasCatchAll && i > catchAllPosition) { throw Object.defineProperty(new Error(`Catch-all must be the last part of the URL in route "${route.pathname}".`), "__NEXT_ERROR_CODE", { value: "E918", enumerable: false, configurable: true }); } if (hasOptionalCatchAllInPath && i > catchAllPosition) { throw Object.defineProperty(new Error(`Optional catch-all must be the last part of the URL in route "${route.pathname}".`), "__NEXT_ERROR_CODE", { value: "E913", enumerable: false, configurable: true }); } } // Check for both required and optional catch-all if (hasCatchAll && hasOptionalCatchAllInPath) { throw Object.defineProperty(new Error(`You cannot use both a required and optional catch-all route at the same level in route "${route.pathname}".`), "__NEXT_ERROR_CODE", { value: "E911", enumerable: false, configurable: true }); } } /** * Validates a single path for internal consistency and returns its segment parameters. */ function parseAndValidateAppPath(path) { // Fast parse the route information. We're expecting this to be a normalized // route, so we pass `true` to the `parseAppRoute` function. const route = (0, _app.parseAppRoute)(path, true); // Slow walk the data from the route in order to validate it. validateAppRoute(route); return route; } /** * Normalizes segments by replacing dynamic segment names with placeholders. * This allows us to compare routes for structural equivalence. * Preserves interception markers so that routes with different markers are not considered ambiguous. * * Examples: * - [slug] -> [*] * - [modalSlug] -> [*] * - [...slug] -> [...*] * - [[...slug]] -> [[...*]] * - (..)test -> (..)test * - (..)[slug] -> (..)[*] */ function normalizeSegments(segments) { return '/' + segments.map((segment)=>{ if (segment.type === 'static') { return segment.name; } // Dynamic segment - normalize the parameter name by replacing the // parameter name with a wildcard. The interception marker is already // included in the segment name, so no special handling is needed. return segment.name.replace(segment.param.paramName, '*'); }).join('/'); } function validateAppPaths(appPaths) { // First, validate each path individually const paramsByPath = new Map(); for (const path of appPaths){ paramsByPath.set(path, parseAndValidateAppPath(path)); } // Group paths by their normalized structure for ambiguity detection const structureMap = new Map(); for (const [path, route] of paramsByPath){ // Check if the last segment is an optional catch-all and check to see if // there is a route with the same specificity that conflicts with it. const lastSegment = route.segments[route.segments.length - 1]; if ((lastSegment == null ? void 0 : lastSegment.type) === 'dynamic' && lastSegment.param.paramType === 'optional-catchall') { const prefixSegments = route.segments.slice(0, -1); const normalizedPrefix = normalizeSegments(prefixSegments); for (const [appPath, appRoute] of paramsByPath){ const normalizedAppPath = normalizeSegments(appRoute.segments); // Special case: root-level optional catch-all // /[[...slug]] has prefix '' which should match '/' if (prefixSegments.length === 0 && appPath === '/') { throw Object.defineProperty(new Error(`You cannot define a route with the same specificity as an optional catch-all route ("${appPath}" and "/[[...${lastSegment.param.paramName}]]").`), "__NEXT_ERROR_CODE", { value: "E925", enumerable: false, configurable: true }); } // General case: compare normalized structures if (normalizedAppPath === normalizedPrefix) { throw Object.defineProperty(new Error(`You cannot define a route with the same specificity as an optional catch-all route ("${appPath}" and "${normalizedPrefix}/[[...${lastSegment.param.paramName}]]").`), "__NEXT_ERROR_CODE", { value: "E917", enumerable: false, configurable: true }); } } } // Normalize the route to get its structure const structure = normalizeSegments(route.segments); // Track which paths map to this structure const existingPaths = structureMap.get(structure) ?? []; existingPaths.push(path); structureMap.set(structure, existingPaths); } // Check for ambiguous routes (different slug names, same structure) const conflicts = []; for (const [structure, paths] of structureMap){ if (paths.length > 1) { // Multiple paths map to the same structure - this is ambiguous conflicts.push({ paths, normalizedPath: structure }); } } if (conflicts.length > 0) { const errorMessages = conflicts.map(({ paths, normalizedPath })=>{ const pathsList = paths.map((p)=>` - ${p}`).join('\n'); return `Ambiguous route pattern "${normalizedPath}" matches multiple routes:\n${pathsList}`; }); throw Object.defineProperty(new Error(`Ambiguous app routes detected:\n\n${errorMessages.join('\n\n')}\n\n` + `These routes cannot be distinguished from each other when matching URLs. ` + `Please ensure that dynamic segments have unique patterns or use different static segments.`), "__NEXT_ERROR_CODE", { value: "E912", enumerable: false, configurable: true }); } return Array.from(paramsByPath.values()); } //# sourceMappingURL=validate-app-paths.js.map