Skip to content

Commit bba1a78

Browse files
committed
feat(build): add validation to detect ambiguous app routes at build time
Introduces validateAppPaths() to catch route conflicts that were previously going undetected until runtime. Routes with different dynamic segment names (e.g., [slug] vs [modalSlug]) that normalize to the same pattern now trigger clear build-time errors with actionable guidance. This prevents subtle routing issues in parallel routes and interception routes where structural equivalence wasn't being validated during the build process.
1 parent e5e80ea commit bba1a78

File tree

7 files changed

+1106
-5
lines changed

7 files changed

+1106
-5
lines changed

packages/next/errors.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,5 +907,22 @@
907907
"906": "Bindings not loaded yet, but they are being loaded, did you forget to await?",
908908
"907": "bindings not loaded yet. Either call `loadBindings` to wait for them to be available or ensure that `installBindings` has already been called.",
909909
"908": "Invalid flags should be run as node detached-flush dev ./path-to/project [eventsFile]",
910-
"909": "Failed to load SWC binary for %s/%s, see more info here: https://nextjs.org/docs/messages/failed-loading-swc"
910+
"909": "Failed to load SWC binary for %s/%s, see more info here: https://nextjs.org/docs/messages/failed-loading-swc",
911+
"910": "Optional route parameters are not yet supported (\"%s\") in route \"%s\".",
912+
"911": "You cannot use both a required and optional catch-all route at the same level in route \"%s\".",
913+
"912": "Ambiguous app routes detected:\\n\\n%s\\n\\nThese routes cannot be distinguished from each other when matching URLs. Please ensure that dynamic segments have unique patterns or use different static segments.",
914+
"913": "Optional catch-all must be the last part of the URL in route \"%s\".",
915+
"914": "You cannot have the same slug name \"%s\" repeat within a single dynamic path in route \"%s\".",
916+
"915": "Detected a three-dot character ('…') at ('%s') in route \"%s\". Did you mean ('...')?",
917+
"916": "Segment names may not start or end with extra brackets ('%s') in route \"%s\".",
918+
"917": "You cannot define a route with the same specificity as an optional catch-all route (\"%s\" and \"%s/[[...%s]]\").",
919+
"918": "Catch-all must be the last part of the URL in route \"%s\".",
920+
"919": "You cannot have the slug names \"%s\" and \"%s\" differ only by non-word symbols within a single dynamic path in route \"%s\".",
921+
"920": "Segment names may not start with erroneous periods ('%s') in route \"%s\".",
922+
"921": "Detected a three-dot character ('…') in parameter \"%s\" in route \"%s\". Did you mean ('...')?",
923+
"922": "Parameter names cannot be empty in route \"%s\".",
924+
"923": "%s is being parsed as a normalized route, but it has a route group or parallel route segment.",
925+
"924": "Invalid interception route: %s",
926+
"925": "You cannot define a route with the same specificity as an optional catch-all route (\"%s\" and \"/[[...%s]]\").",
927+
"926": "Optional route parameters are not yet supported (\"[%s]\") in route \"%s\"."
911928
}

packages/next/src/build/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ import {
230230
writeValidatorFile,
231231
} from '../server/lib/router-utils/route-types-utils'
232232
import { Lockfile } from './lockfile'
233+
import { validateAppPaths } from './validate-app-paths'
233234

234235
type Fallback = null | boolean | string
235236

@@ -1406,6 +1407,13 @@ export default async function build(
14061407
}
14071408

14081409
const appPaths = Array.from(appPageKeys)
1410+
1411+
// Validate that the app paths are valid. This is currently duplicating
1412+
// the logic from packages/next/src/shared/lib/router/utils/sorted-routes.ts
1413+
// but is instead specifically focused on code that can be shared
1414+
// eventually with the development code.
1415+
validateAppPaths(appPaths)
1416+
14091417
// Interception routes are modelled as beforeFiles rewrites
14101418
rewrites.beforeFiles.push(
14111419
...generateInterceptionRoutesRewrites(appPaths, config.basePath)

0 commit comments

Comments
 (0)