import { bold, yellow } from "./picocolors"; import { escapeStringRegexp } from "../shared/lib/escape-regexp"; import { tryToParsePath } from "./try-to-parse-path"; import { allowedStatusCodes } from "./redirect-status"; const allowedHasTypes = new Set([ "header", "cookie", "query", "host" ]); const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g; export function normalizeRouteRegex(regex) { // clean up un-necessary escaping from regex.source which turns / into \\/ return regex.replace(/\\\//g, "/"); } function checkRedirect(route) { const invalidParts = []; let hadInvalidStatus = false; if (route.statusCode && !allowedStatusCodes.has(route["statusCode"])) { hadInvalidStatus = true; invalidParts.push(`\`statusCode\` is not undefined or valid statusCode`); } if (typeof route.permanent !== "boolean" && !route["statusCode"]) { invalidParts.push(`\`permanent\` is not set to \`true\` or \`false\``); } return { invalidParts, hadInvalidStatus }; } function checkHeader(route) { const invalidParts = []; if (!Array.isArray(route.headers)) { invalidParts.push("`headers` field must be an array"); } else if (route.headers.length === 0) { invalidParts.push("`headers` field cannot be empty"); } else { for (const header of route.headers){ if (!header || typeof header !== "object") { invalidParts.push("`headers` items must be object with { key: '', value: '' }"); break; } if (typeof header.key !== "string") { invalidParts.push("`key` in header item must be string"); break; } if (typeof header.value !== "string") { invalidParts.push("`value` in header item must be string"); break; } } } return invalidParts; } export function checkCustomRoutes(routes, type) { if (!Array.isArray(routes)) { console.error(`Error: ${type}s must return an array, received ${typeof routes}.\n` + `See here for more info: https://nextjs.org/docs/messages/routes-must-be-array`); process.exit(1); } let numInvalidRoutes = 0; let hadInvalidStatus = false; let hadInvalidHas = false; let hadInvalidMissing = false; const allowedKeys = new Set([ "source", "locale", "has", "missing" ]); if (type === "rewrite") { allowedKeys.add("basePath"); allowedKeys.add("destination"); } if (type === "redirect") { allowedKeys.add("basePath"); allowedKeys.add("statusCode"); allowedKeys.add("permanent"); allowedKeys.add("destination"); } if (type === "header") { allowedKeys.add("basePath"); allowedKeys.add("headers"); } for (const route of routes){ if (!route || typeof route !== "object") { console.error(`The route ${JSON.stringify(route)} is not a valid object with \`source\`${type !== "middleware" ? ` and \`${type === "header" ? "headers" : "destination"}\`` : ""}`); numInvalidRoutes++; continue; } if (type === "rewrite" && route.basePath === false && !(route.destination.startsWith("http://") || route.destination.startsWith("https://"))) { console.error(`The route ${route.source} rewrites urls outside of the basePath. Please use a destination that starts with \`http://\` or \`https://\` https://nextjs.org/docs/messages/invalid-external-rewrite`); numInvalidRoutes++; continue; } const keys = Object.keys(route); const invalidKeys = keys.filter((key)=>!allowedKeys.has(key)); const invalidParts = []; if ("basePath" in route && typeof route.basePath !== "undefined" && route.basePath !== false) { invalidParts.push("`basePath` must be undefined or false"); } if (typeof route.locale !== "undefined" && route.locale !== false) { invalidParts.push("`locale` must be undefined or false"); } const checkInvalidHasMissing = (items, fieldName)=>{ let hadInvalidItem = false; if (typeof items !== "undefined" && !Array.isArray(items)) { invalidParts.push(`\`${fieldName}\` must be undefined or valid has object`); hadInvalidItem = true; } else if (items) { const invalidHasItems = []; for (const hasItem of items){ let invalidHasParts = []; if (!allowedHasTypes.has(hasItem.type)) { invalidHasParts.push(`invalid type "${hasItem.type}"`); } if (typeof hasItem.key !== "string" && hasItem.type !== "host") { invalidHasParts.push(`invalid key "${hasItem.key}"`); } if (typeof hasItem.value !== "undefined" && typeof hasItem.value !== "string") { invalidHasParts.push(`invalid value "${hasItem.value}"`); } if (typeof hasItem.value === "undefined" && hasItem.type === "host") { invalidHasParts.push(`value is required for "host" type`); } if (invalidHasParts.length > 0) { invalidHasItems.push(`${invalidHasParts.join(", ")} for ${JSON.stringify(hasItem)}`); } } if (invalidHasItems.length > 0) { hadInvalidItem = true; const itemStr = `item${invalidHasItems.length === 1 ? "" : "s"}`; console.error(`Invalid \`${fieldName}\` ${itemStr}:\n` + invalidHasItems.join("\n")); console.error(); invalidParts.push(`invalid \`${fieldName}\` ${itemStr} found`); } } return hadInvalidItem; }; if (checkInvalidHasMissing(route.has, "has")) { hadInvalidHas = true; } if (checkInvalidHasMissing(route.missing, "missing")) { hadInvalidMissing = true; } if (!route.source) { invalidParts.push("`source` is missing"); } else if (typeof route.source !== "string") { invalidParts.push("`source` is not a string"); } else if (!route.source.startsWith("/")) { invalidParts.push("`source` does not start with /"); } if (type === "header") { invalidParts.push(...checkHeader(route)); } else if (type !== "middleware") { let _route = route; if (!_route.destination) { invalidParts.push("`destination` is missing"); } else if (typeof _route.destination !== "string") { invalidParts.push("`destination` is not a string"); } else if (type === "rewrite" && !_route.destination.match(/^(\/|https:\/\/|http:\/\/)/)) { invalidParts.push("`destination` does not start with `/`, `http://`, or `https://`"); } } if (type === "redirect") { const result = checkRedirect(route); hadInvalidStatus = hadInvalidStatus || result.hadInvalidStatus; invalidParts.push(...result.invalidParts); } let sourceTokens; if (typeof route.source === "string" && route.source.startsWith("/")) { // only show parse error if we didn't already show error // for not being a string const { tokens, error, regexStr } = tryToParsePath(route.source); if (error) { invalidParts.push("`source` parse failed"); } if (regexStr && regexStr.length > 4096) { invalidParts.push("`source` exceeds max built length of 4096"); } sourceTokens = tokens; } const hasSegments = new Set(); if (route.has) { for (const hasItem of route.has){ if (!hasItem.value && hasItem.key) { hasSegments.add(hasItem.key); } if (hasItem.value) { for (const match of hasItem.value.matchAll(namedGroupsRegex)){ if (match[1]) { hasSegments.add(match[1]); } } if (hasItem.type === "host") { hasSegments.add("host"); } } } } // make sure no unnamed patterns are attempted to be used in the // destination as this can cause confusion and is not allowed if (typeof route.destination === "string") { if (route.destination.startsWith("/") && Array.isArray(sourceTokens)) { const unnamedInDest = new Set(); for (const token of sourceTokens){ if (typeof token === "object" && typeof token.name === "number") { const unnamedIndex = new RegExp(`:${token.name}(?!\\d)`); if (route.destination.match(unnamedIndex)) { unnamedInDest.add(`:${token.name}`); } } } if (unnamedInDest.size > 0) { invalidParts.push(`\`destination\` has unnamed params ${[ ...unnamedInDest ].join(", ")}`); } else { const { tokens: destTokens, regexStr: destRegexStr, error: destinationParseFailed } = tryToParsePath(route.destination, { handleUrl: true }); if (destRegexStr && destRegexStr.length > 4096) { invalidParts.push("`destination` exceeds max built length of 4096"); } if (destinationParseFailed) { invalidParts.push("`destination` parse failed"); } else { const sourceSegments = new Set(sourceTokens.map((item)=>typeof item === "object" && item.name).filter(Boolean)); const invalidDestSegments = new Set(); for (const token of destTokens){ if (typeof token === "object" && !sourceSegments.has(token.name) && !hasSegments.has(token.name)) { invalidDestSegments.add(token.name); } } if (invalidDestSegments.size) { invalidParts.push(`\`destination\` has segments not in \`source\` or \`has\` (${[ ...invalidDestSegments ].join(", ")})`); } } } } } const hasInvalidKeys = invalidKeys.length > 0; const hasInvalidParts = invalidParts.length > 0; if (hasInvalidKeys || hasInvalidParts) { console.error(`${invalidParts.join(", ")}${invalidKeys.length ? (hasInvalidParts ? "," : "") + ` invalid field${invalidKeys.length === 1 ? "" : "s"}: ` + invalidKeys.join(",") : ""} for route ${JSON.stringify(route)}`); console.error(); numInvalidRoutes++; } } if (numInvalidRoutes > 0) { if (hadInvalidStatus) { console.error(`\nValid redirect statusCode values are ${[ ...allowedStatusCodes ].join(", ")}`); } if (hadInvalidHas) { console.error(`\nValid \`has\` object shape is ${JSON.stringify({ type: [ ...allowedHasTypes ].join(", "), key: "the key to check for", value: "undefined or a value string to match against" }, null, 2)}`); } if (hadInvalidMissing) { console.error(`\nValid \`missing\` object shape is ${JSON.stringify({ type: [ ...allowedHasTypes ].join(", "), key: "the key to check for", value: "undefined or a value string to match against" }, null, 2)}`); } console.error(); console.error(`Error: Invalid ${type}${numInvalidRoutes === 1 ? "" : "s"} found`); process.exit(1); } } function processRoutes(routes, config, type) { const _routes = routes; const newRoutes = []; const defaultLocales = []; if (config.i18n && type === "redirect") { var _config_i18n; for (const item of ((_config_i18n = config.i18n) == null ? void 0 : _config_i18n.domains) || []){ defaultLocales.push({ locale: item.defaultLocale, base: `http${item.http ? "" : "s"}://${item.domain}` }); } defaultLocales.push({ locale: config.i18n.defaultLocale, base: "" }); } for (const r of _routes){ var _r_destination; const srcBasePath = config.basePath && r.basePath !== false ? config.basePath : ""; const isExternal = !((_r_destination = r.destination) == null ? void 0 : _r_destination.startsWith("/")); const destBasePath = srcBasePath && !isExternal ? srcBasePath : ""; if (config.i18n && r.locale !== false) { var _r_destination1; if (!isExternal) { defaultLocales.forEach((item)=>{ let destination; if (r.destination) { destination = item.base ? `${item.base}${destBasePath}${r.destination}` : `${destBasePath}${r.destination}`; } newRoutes.push({ ...r, destination, source: `${srcBasePath}/${item.locale}${r.source === "/" && !config.trailingSlash ? "" : r.source}` }); }); } r.source = `/:nextInternalLocale(${config.i18n.locales.map((locale)=>escapeStringRegexp(locale)).join("|")})${r.source === "/" && !config.trailingSlash ? "" : r.source}`; if (r.destination && ((_r_destination1 = r.destination) == null ? void 0 : _r_destination1.startsWith("/"))) { r.destination = `/:nextInternalLocale${r.destination === "/" && !config.trailingSlash ? "" : r.destination}`; } } r.source = `${srcBasePath}${r.source === "/" && srcBasePath ? "" : r.source}`; if (r.destination) { r.destination = `${destBasePath}${r.destination === "/" && destBasePath ? "" : r.destination}`; } newRoutes.push(r); } return newRoutes; } async function loadRedirects(config) { if (typeof config.redirects !== "function") { return []; } let redirects = await config.redirects(); // check before we process the routes and after to ensure // they are still valid checkCustomRoutes(redirects, "redirect"); // save original redirects before transforms if (Array.isArray(redirects)) { config._originalRedirects = redirects.map((r)=>({ ...r })); } redirects = processRoutes(redirects, config, "redirect"); checkCustomRoutes(redirects, "redirect"); return redirects; } async function loadRewrites(config) { if (typeof config.rewrites !== "function") { return { beforeFiles: [], afterFiles: [], fallback: [] }; } const _rewrites = await config.rewrites(); let beforeFiles = []; let afterFiles = []; let fallback = []; if (!Array.isArray(_rewrites) && typeof _rewrites === "object" && Object.keys(_rewrites).every((key)=>key === "beforeFiles" || key === "afterFiles" || key === "fallback")) { beforeFiles = _rewrites.beforeFiles || []; afterFiles = _rewrites.afterFiles || []; fallback = _rewrites.fallback || []; } else { afterFiles = _rewrites; } // check before we process the routes and after to ensure // they are still valid checkCustomRoutes(beforeFiles, "rewrite"); checkCustomRoutes(afterFiles, "rewrite"); checkCustomRoutes(fallback, "rewrite"); // save original rewrites before transforms config._originalRewrites = { beforeFiles: beforeFiles.map((r)=>({ ...r })), afterFiles: afterFiles.map((r)=>({ ...r })), fallback: fallback.map((r)=>({ ...r })) }; beforeFiles = processRoutes(beforeFiles, config, "rewrite"); afterFiles = processRoutes(afterFiles, config, "rewrite"); fallback = processRoutes(fallback, config, "rewrite"); checkCustomRoutes(beforeFiles, "rewrite"); checkCustomRoutes(afterFiles, "rewrite"); checkCustomRoutes(fallback, "rewrite"); return { beforeFiles, afterFiles, fallback }; } async function loadHeaders(config) { if (typeof config.headers !== "function") { return []; } let headers = await config.headers(); // check before we process the routes and after to ensure // they are still valid checkCustomRoutes(headers, "header"); headers = processRoutes(headers, config, "header"); checkCustomRoutes(headers, "header"); return headers; } export default async function loadCustomRoutes(config) { const [headers, rewrites, redirects] = await Promise.all([ loadHeaders(config), loadRewrites(config), loadRedirects(config) ]); const totalRewrites = rewrites.beforeFiles.length + rewrites.afterFiles.length + rewrites.fallback.length; const totalRoutes = headers.length + redirects.length + totalRewrites; if (totalRoutes > 1000) { console.warn(bold(yellow(`Warning: `)) + `total number of custom routes exceeds 1000, this can reduce performance. Route counts:\n` + `headers: ${headers.length}\n` + `rewrites: ${totalRewrites}\n` + `redirects: ${redirects.length}\n` + `See more info: https://nextjs.org/docs/messages/max-custom-routes-reached`); } if (!config.skipTrailingSlashRedirect) { if (config.trailingSlash) { redirects.unshift({ source: "/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/", destination: "/:file", permanent: true, locale: config.i18n ? false : undefined, internal: true, // don't run this redirect for _next/data requests missing: [ { type: "header", key: "x-nextjs-data" } ] }, { source: "/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)", destination: "/:notfile/", permanent: true, locale: config.i18n ? false : undefined, internal: true }); if (config.basePath) { redirects.unshift({ source: config.basePath, destination: config.basePath + "/", permanent: true, basePath: false, locale: config.i18n ? false : undefined, internal: true }); } } else { redirects.unshift({ source: "/:path+/", destination: "/:path+", permanent: true, locale: config.i18n ? false : undefined, internal: true }); if (config.basePath) { redirects.unshift({ source: config.basePath + "/", destination: config.basePath, permanent: true, basePath: false, locale: config.i18n ? false : undefined, internal: true }); } } } return { headers, rewrites, redirects }; } //# sourceMappingURL=load-custom-routes.js.map