load-custom-routes.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = loadCustomRoutes;
  6. exports.normalizeRouteRegex = normalizeRouteRegex;
  7. exports.checkCustomRoutes = checkCustomRoutes;
  8. var _chalk = _interopRequireDefault(require("./chalk"));
  9. var _escapeRegexp = require("../shared/lib/escape-regexp");
  10. var _tryToParsePath = require("./try-to-parse-path");
  11. var _redirectStatus = require("./redirect-status");
  12. async function loadCustomRoutes(config) {
  13. var ref;
  14. const [headers, rewrites, redirects] = await Promise.all([
  15. loadHeaders(config),
  16. loadRewrites(config),
  17. loadRedirects(config),
  18. ]);
  19. const totalRewrites = rewrites.beforeFiles.length + rewrites.afterFiles.length + rewrites.fallback.length;
  20. const totalRoutes = headers.length + redirects.length + totalRewrites;
  21. if (totalRoutes > 1000) {
  22. console.warn(_chalk.default.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`);
  23. }
  24. if (!((ref = config.experimental) == null ? void 0 : ref.skipTrailingSlashRedirect)) {
  25. if (config.trailingSlash) {
  26. redirects.unshift({
  27. source: "/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/",
  28. destination: "/:file",
  29. permanent: true,
  30. locale: config.i18n ? false : undefined,
  31. internal: true
  32. }, {
  33. source: "/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)",
  34. destination: "/:notfile/",
  35. permanent: true,
  36. locale: config.i18n ? false : undefined,
  37. internal: true
  38. });
  39. if (config.basePath) {
  40. redirects.unshift({
  41. source: config.basePath,
  42. destination: config.basePath + "/",
  43. permanent: true,
  44. basePath: false,
  45. locale: config.i18n ? false : undefined,
  46. internal: true
  47. });
  48. }
  49. } else {
  50. redirects.unshift({
  51. source: "/:path+/",
  52. destination: "/:path+",
  53. permanent: true,
  54. locale: config.i18n ? false : undefined,
  55. internal: true
  56. });
  57. if (config.basePath) {
  58. redirects.unshift({
  59. source: config.basePath + "/",
  60. destination: config.basePath,
  61. permanent: true,
  62. basePath: false,
  63. locale: config.i18n ? false : undefined,
  64. internal: true
  65. });
  66. }
  67. }
  68. }
  69. return {
  70. headers,
  71. rewrites,
  72. redirects
  73. };
  74. }
  75. function _interopRequireDefault(obj) {
  76. return obj && obj.__esModule ? obj : {
  77. default: obj
  78. };
  79. }
  80. const allowedHasTypes = new Set([
  81. "header",
  82. "cookie",
  83. "query",
  84. "host"
  85. ]);
  86. const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g;
  87. function normalizeRouteRegex(regex) {
  88. // clean up un-necessary escaping from regex.source which turns / into \\/
  89. return regex.replace(/\\\//g, "/");
  90. }
  91. function checkRedirect(route) {
  92. const invalidParts = [];
  93. let hadInvalidStatus = false;
  94. if (route.statusCode && !_redirectStatus.allowedStatusCodes.has(route.statusCode)) {
  95. hadInvalidStatus = true;
  96. invalidParts.push(`\`statusCode\` is not undefined or valid statusCode`);
  97. }
  98. if (typeof route.permanent !== "boolean" && !route.statusCode) {
  99. invalidParts.push(`\`permanent\` is not set to \`true\` or \`false\``);
  100. }
  101. return {
  102. invalidParts,
  103. hadInvalidStatus
  104. };
  105. }
  106. function checkHeader(route) {
  107. const invalidParts = [];
  108. if (!Array.isArray(route.headers)) {
  109. invalidParts.push("`headers` field must be an array");
  110. } else if (route.headers.length === 0) {
  111. invalidParts.push("`headers` field cannot be empty");
  112. } else {
  113. for (const header of route.headers){
  114. if (!header || typeof header !== "object") {
  115. invalidParts.push("`headers` items must be object with { key: '', value: '' }");
  116. break;
  117. }
  118. if (typeof header.key !== "string") {
  119. invalidParts.push("`key` in header item must be string");
  120. break;
  121. }
  122. if (typeof header.value !== "string") {
  123. invalidParts.push("`value` in header item must be string");
  124. break;
  125. }
  126. }
  127. }
  128. return invalidParts;
  129. }
  130. function checkCustomRoutes(routes, type) {
  131. if (!Array.isArray(routes)) {
  132. 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`);
  133. process.exit(1);
  134. }
  135. let numInvalidRoutes = 0;
  136. let hadInvalidStatus = false;
  137. let hadInvalidHas = false;
  138. let hadInvalidMissing = false;
  139. const allowedKeys = new Set([
  140. "source",
  141. "locale",
  142. "has",
  143. "missing"
  144. ]);
  145. if (type === "rewrite") {
  146. allowedKeys.add("basePath");
  147. allowedKeys.add("destination");
  148. }
  149. if (type === "redirect") {
  150. allowedKeys.add("basePath");
  151. allowedKeys.add("statusCode");
  152. allowedKeys.add("permanent");
  153. allowedKeys.add("destination");
  154. }
  155. if (type === "header") {
  156. allowedKeys.add("basePath");
  157. allowedKeys.add("headers");
  158. }
  159. for (const route of routes){
  160. if (!route || typeof route !== "object") {
  161. console.error(`The route ${JSON.stringify(route)} is not a valid object with \`source\`${type !== "middleware" ? ` and \`${type === "header" ? "headers" : "destination"}\`` : ""}`);
  162. numInvalidRoutes++;
  163. continue;
  164. }
  165. if (type === "rewrite" && route.basePath === false && !(route.destination.startsWith("http://") || route.destination.startsWith("https://"))) {
  166. 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`);
  167. numInvalidRoutes++;
  168. continue;
  169. }
  170. const keys = Object.keys(route);
  171. const invalidKeys = keys.filter((key)=>!allowedKeys.has(key));
  172. const invalidParts = [];
  173. if ("basePath" in route && typeof route.basePath !== "undefined" && route.basePath !== false) {
  174. invalidParts.push("`basePath` must be undefined or false");
  175. }
  176. if (typeof route.locale !== "undefined" && route.locale !== false) {
  177. invalidParts.push("`locale` must be undefined or false");
  178. }
  179. const checkInvalidHasMissing = (items, fieldName)=>{
  180. let hadInvalidItem = false;
  181. if (typeof items !== "undefined" && !Array.isArray(items)) {
  182. invalidParts.push(`\`${fieldName}\` must be undefined or valid has object`);
  183. hadInvalidItem = true;
  184. } else if (items) {
  185. const invalidHasItems = [];
  186. for (const hasItem of items){
  187. let invalidHasParts = [];
  188. if (!allowedHasTypes.has(hasItem.type)) {
  189. invalidHasParts.push(`invalid type "${hasItem.type}"`);
  190. }
  191. if (typeof hasItem.key !== "string" && hasItem.type !== "host") {
  192. invalidHasParts.push(`invalid key "${hasItem.key}"`);
  193. }
  194. if (typeof hasItem.value !== "undefined" && typeof hasItem.value !== "string") {
  195. invalidHasParts.push(`invalid value "${hasItem.value}"`);
  196. }
  197. if (typeof hasItem.value === "undefined" && hasItem.type === "host") {
  198. invalidHasParts.push(`value is required for "host" type`);
  199. }
  200. if (invalidHasParts.length > 0) {
  201. invalidHasItems.push(`${invalidHasParts.join(", ")} for ${JSON.stringify(hasItem)}`);
  202. }
  203. }
  204. if (invalidHasItems.length > 0) {
  205. hadInvalidItem = true;
  206. const itemStr = `item${invalidHasItems.length === 1 ? "" : "s"}`;
  207. console.error(`Invalid \`${fieldName}\` ${itemStr}:\n` + invalidHasItems.join("\n"));
  208. console.error();
  209. invalidParts.push(`invalid \`${fieldName}\` ${itemStr} found`);
  210. }
  211. }
  212. return hadInvalidItem;
  213. };
  214. if (checkInvalidHasMissing(route.has, "has")) {
  215. hadInvalidHas = true;
  216. }
  217. if (checkInvalidHasMissing(route.missing, "missing")) {
  218. hadInvalidMissing = true;
  219. }
  220. if (!route.source) {
  221. invalidParts.push("`source` is missing");
  222. } else if (typeof route.source !== "string") {
  223. invalidParts.push("`source` is not a string");
  224. } else if (!route.source.startsWith("/")) {
  225. invalidParts.push("`source` does not start with /");
  226. }
  227. if (type === "header") {
  228. invalidParts.push(...checkHeader(route));
  229. } else if (type !== "middleware") {
  230. let _route = route;
  231. if (!_route.destination) {
  232. invalidParts.push("`destination` is missing");
  233. } else if (typeof _route.destination !== "string") {
  234. invalidParts.push("`destination` is not a string");
  235. } else if (type === "rewrite" && !_route.destination.match(/^(\/|https:\/\/|http:\/\/)/)) {
  236. invalidParts.push("`destination` does not start with `/`, `http://`, or `https://`");
  237. }
  238. }
  239. if (type === "redirect") {
  240. const result = checkRedirect(route);
  241. hadInvalidStatus = hadInvalidStatus || result.hadInvalidStatus;
  242. invalidParts.push(...result.invalidParts);
  243. }
  244. let sourceTokens;
  245. if (typeof route.source === "string" && route.source.startsWith("/")) {
  246. // only show parse error if we didn't already show error
  247. // for not being a string
  248. const { tokens , error , regexStr } = (0, _tryToParsePath).tryToParsePath(route.source);
  249. if (error) {
  250. invalidParts.push("`source` parse failed");
  251. }
  252. if (regexStr && regexStr.length > 4096) {
  253. invalidParts.push("`source` exceeds max built length of 4096");
  254. }
  255. sourceTokens = tokens;
  256. }
  257. const hasSegments = new Set();
  258. if (route.has) {
  259. for (const hasItem of route.has){
  260. if (!hasItem.value && hasItem.key) {
  261. hasSegments.add(hasItem.key);
  262. }
  263. if (hasItem.value) {
  264. for (const match of hasItem.value.matchAll(namedGroupsRegex)){
  265. if (match[1]) {
  266. hasSegments.add(match[1]);
  267. }
  268. }
  269. if (hasItem.type === "host") {
  270. hasSegments.add("host");
  271. }
  272. }
  273. }
  274. }
  275. // make sure no unnamed patterns are attempted to be used in the
  276. // destination as this can cause confusion and is not allowed
  277. if (typeof route.destination === "string") {
  278. if (route.destination.startsWith("/") && Array.isArray(sourceTokens)) {
  279. const unnamedInDest = new Set();
  280. for (const token of sourceTokens){
  281. if (typeof token === "object" && typeof token.name === "number") {
  282. const unnamedIndex = new RegExp(`:${token.name}(?!\\d)`);
  283. if (route.destination.match(unnamedIndex)) {
  284. unnamedInDest.add(`:${token.name}`);
  285. }
  286. }
  287. }
  288. if (unnamedInDest.size > 0) {
  289. invalidParts.push(`\`destination\` has unnamed params ${[
  290. ...unnamedInDest
  291. ].join(", ")}`);
  292. } else {
  293. const { tokens: destTokens , regexStr: destRegexStr , error: destinationParseFailed , } = (0, _tryToParsePath).tryToParsePath(route.destination, {
  294. handleUrl: true
  295. });
  296. if (destRegexStr && destRegexStr.length > 4096) {
  297. invalidParts.push("`destination` exceeds max built length of 4096");
  298. }
  299. if (destinationParseFailed) {
  300. invalidParts.push("`destination` parse failed");
  301. } else {
  302. const sourceSegments = new Set(sourceTokens.map((item)=>typeof item === "object" && item.name).filter(Boolean));
  303. const invalidDestSegments = new Set();
  304. for (const token of destTokens){
  305. if (typeof token === "object" && !sourceSegments.has(token.name) && !hasSegments.has(token.name)) {
  306. invalidDestSegments.add(token.name);
  307. }
  308. }
  309. if (invalidDestSegments.size) {
  310. invalidParts.push(`\`destination\` has segments not in \`source\` or \`has\` (${[
  311. ...invalidDestSegments,
  312. ].join(", ")})`);
  313. }
  314. }
  315. }
  316. }
  317. }
  318. const hasInvalidKeys = invalidKeys.length > 0;
  319. const hasInvalidParts = invalidParts.length > 0;
  320. if (hasInvalidKeys || hasInvalidParts) {
  321. console.error(`${invalidParts.join(", ")}${invalidKeys.length ? (hasInvalidParts ? "," : "") + ` invalid field${invalidKeys.length === 1 ? "" : "s"}: ` + invalidKeys.join(",") : ""} for route ${JSON.stringify(route)}`);
  322. console.error();
  323. numInvalidRoutes++;
  324. }
  325. }
  326. if (numInvalidRoutes > 0) {
  327. if (hadInvalidStatus) {
  328. console.error(`\nValid redirect statusCode values are ${[
  329. ..._redirectStatus.allowedStatusCodes
  330. ].join(", ")}`);
  331. }
  332. if (hadInvalidHas) {
  333. console.error(`\nValid \`has\` object shape is ${JSON.stringify({
  334. type: [
  335. ...allowedHasTypes
  336. ].join(", "),
  337. key: "the key to check for",
  338. value: "undefined or a value string to match against"
  339. }, null, 2)}`);
  340. }
  341. if (hadInvalidMissing) {
  342. console.error(`\nValid \`missing\` object shape is ${JSON.stringify({
  343. type: [
  344. ...allowedHasTypes
  345. ].join(", "),
  346. key: "the key to check for",
  347. value: "undefined or a value string to match against"
  348. }, null, 2)}`);
  349. }
  350. console.error();
  351. console.error(`Error: Invalid ${type}${numInvalidRoutes === 1 ? "" : "s"} found`);
  352. process.exit(1);
  353. }
  354. }
  355. function processRoutes(routes, config, type) {
  356. const _routes = routes;
  357. const newRoutes = [];
  358. const defaultLocales = [];
  359. if (config.i18n && type === "redirect") {
  360. var ref;
  361. for (const item of ((ref = config.i18n) == null ? void 0 : ref.domains) || []){
  362. defaultLocales.push({
  363. locale: item.defaultLocale,
  364. base: `http${item.http ? "" : "s"}://${item.domain}`
  365. });
  366. }
  367. defaultLocales.push({
  368. locale: config.i18n.defaultLocale,
  369. base: ""
  370. });
  371. }
  372. for (const r of _routes){
  373. var ref1;
  374. const srcBasePath = config.basePath && r.basePath !== false ? config.basePath : "";
  375. const isExternal = !((ref1 = r.destination) == null ? void 0 : ref1.startsWith("/"));
  376. const destBasePath = srcBasePath && !isExternal ? srcBasePath : "";
  377. if (config.i18n && r.locale !== false) {
  378. var ref2;
  379. if (!isExternal) {
  380. defaultLocales.forEach((item)=>{
  381. let destination;
  382. if (r.destination) {
  383. destination = item.base ? `${item.base}${destBasePath}${r.destination}` : `${destBasePath}${r.destination}`;
  384. }
  385. newRoutes.push({
  386. ...r,
  387. destination,
  388. source: `${srcBasePath}/${item.locale}${r.source}`
  389. });
  390. });
  391. }
  392. r.source = `/:nextInternalLocale(${config.i18n.locales.map((locale)=>(0, _escapeRegexp).escapeStringRegexp(locale)).join("|")})${r.source === "/" && !config.trailingSlash ? "" : r.source}`;
  393. if (r.destination && ((ref2 = r.destination) == null ? void 0 : ref2.startsWith("/"))) {
  394. r.destination = `/:nextInternalLocale${r.destination === "/" && !config.trailingSlash ? "" : r.destination}`;
  395. }
  396. }
  397. r.source = `${srcBasePath}${r.source === "/" && srcBasePath ? "" : r.source}`;
  398. if (r.destination) {
  399. r.destination = `${destBasePath}${r.destination === "/" && destBasePath ? "" : r.destination}`;
  400. }
  401. newRoutes.push(r);
  402. }
  403. return newRoutes;
  404. }
  405. async function loadRedirects(config) {
  406. if (typeof config.redirects !== "function") {
  407. return [];
  408. }
  409. let redirects = await config.redirects();
  410. // check before we process the routes and after to ensure
  411. // they are still valid
  412. checkCustomRoutes(redirects, "redirect");
  413. redirects = processRoutes(redirects, config, "redirect");
  414. checkCustomRoutes(redirects, "redirect");
  415. return redirects;
  416. }
  417. async function loadRewrites(config) {
  418. if (typeof config.rewrites !== "function") {
  419. return {
  420. beforeFiles: [],
  421. afterFiles: [],
  422. fallback: []
  423. };
  424. }
  425. const _rewrites = await config.rewrites();
  426. let beforeFiles = [];
  427. let afterFiles = [];
  428. let fallback = [];
  429. if (!Array.isArray(_rewrites) && typeof _rewrites === "object" && Object.keys(_rewrites).every((key)=>key === "beforeFiles" || key === "afterFiles" || key === "fallback")) {
  430. beforeFiles = _rewrites.beforeFiles || [];
  431. afterFiles = _rewrites.afterFiles || [];
  432. fallback = _rewrites.fallback || [];
  433. } else {
  434. afterFiles = _rewrites;
  435. }
  436. // check before we process the routes and after to ensure
  437. // they are still valid
  438. checkCustomRoutes(beforeFiles, "rewrite");
  439. checkCustomRoutes(afterFiles, "rewrite");
  440. checkCustomRoutes(fallback, "rewrite");
  441. beforeFiles = processRoutes(beforeFiles, config, "rewrite");
  442. afterFiles = processRoutes(afterFiles, config, "rewrite");
  443. fallback = processRoutes(fallback, config, "rewrite");
  444. checkCustomRoutes(beforeFiles, "rewrite");
  445. checkCustomRoutes(afterFiles, "rewrite");
  446. checkCustomRoutes(fallback, "rewrite");
  447. return {
  448. beforeFiles,
  449. afterFiles,
  450. fallback
  451. };
  452. }
  453. async function loadHeaders(config) {
  454. if (typeof config.headers !== "function") {
  455. return [];
  456. }
  457. let headers = await config.headers();
  458. // check before we process the routes and after to ensure
  459. // they are still valid
  460. checkCustomRoutes(headers, "header");
  461. headers = processRoutes(headers, config, "header");
  462. checkCustomRoutes(headers, "header");
  463. return headers;
  464. }
  465. //# sourceMappingURL=load-custom-routes.js.map