UNPKG

next

Version:

The React Framework

511 lines (510 loc) • 21.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = loadCustomRoutes; exports.getRedirectStatus = getRedirectStatus; exports.normalizeRouteRegex = normalizeRouteRegex; exports.modifyRouteRegex = modifyRouteRegex; exports.allowedStatusCodes = void 0; var _chalk = _interopRequireDefault(require("./chalk")); var _url = require("url"); var pathToRegexp = _interopRequireWildcard(require("next/dist/compiled/path-to-regexp")); var _escapeRegexp = require("../shared/lib/escape-regexp"); var _constants = require("../shared/lib/constants"); var _isError = _interopRequireDefault(require("./is-error")); 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(_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`); } if (config.trailingSlash) { redirects.unshift({ source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/', destination: '/:file', permanent: true, locale: config.i18n ? false : undefined, internal: true }, { 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 }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for(var key in obj){ if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } const allowedStatusCodes = new Set([ 301, 302, 303, 307, 308 ]); exports.allowedStatusCodes = allowedStatusCodes; const allowedHasTypes = new Set([ 'header', 'cookie', 'query', 'host' ]); const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g; function getRedirectStatus(route) { return route.statusCode || (route.permanent ? _constants.PERMANENT_REDIRECT_STATUS : _constants.TEMPORARY_REDIRECT_STATUS); } function normalizeRouteRegex(regex) { // clean up un-necessary escaping from regex.source which turns / into \\/ return regex.replace(/\\\//g, '/'); } function modifyRouteRegex(regex, restrictedPaths) { if (restrictedPaths) { regex = regex.replace(/\^/, `^(?!${restrictedPaths.map((path)=>path.replace(/\//g, '\\/') ).join('|')})`); } regex = regex.replace(/\$$/, '(?:\\/)?$'); return regex; } 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; } function tryParsePath(route, handleUrl) { const result = {}; let routePath = route; try { if (handleUrl) { const parsedDestination = (0, _url).parse(route, true); routePath = `${parsedDestination.pathname}${parsedDestination.hash || ''}`; } // Make sure we can parse the source properly result.tokens = pathToRegexp.parse(routePath); const regex = pathToRegexp.tokensToRegexp(result.tokens); result.regexStr = regex.source; } catch (err) { // If there is an error show our error link but still show original error or a formatted one if we can let errMatches; if ((0, _isError).default(err) && (errMatches = err.message.match(/at (\d{0,})/))) { const position = parseInt(errMatches[1], 10); console.error(`\nError parsing \`${route}\` ` + `https://nextjs.org/docs/messages/invalid-route-source\n` + `Reason: ${err.message}\n\n` + ` ${routePath}\n` + ` ${new Array(position).fill(' ').join('')}^\n`); } else { console.error(`\nError parsing ${route} https://nextjs.org/docs/messages/invalid-route-source`, err); } result.error = true; } return result; } 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; const allowedKeys = new Set([ 'source', 'basePath', 'locale', 'has' ]); if (type === 'rewrite') { allowedKeys.add('destination'); } if (type === 'redirect') { allowedKeys.add('statusCode'); allowedKeys.add('permanent'); allowedKeys.add('destination'); } if (type === 'header') { 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\` 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 (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'); } if (typeof route.has !== 'undefined' && !Array.isArray(route.has)) { invalidParts.push('`has` must be undefined or valid has object'); hadInvalidHas = true; } else if (route.has) { const invalidHasItems = []; for (const hasItem of route.has){ 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) { hadInvalidHas = true; const itemStr = `item${invalidHasItems.length === 1 ? '' : 's'}`; console.error(`Invalid \`has\` ${itemStr}:\n` + invalidHasItems.join('\n')); console.error(); invalidParts.push(`invalid \`has\` ${itemStr} found`); } } 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 { 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 } = tryParsePath(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 , } = tryParsePath(route.destination, 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)}`); } 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 ref; for (const item of ((ref = config.i18n) === null || ref === void 0 ? void 0 : ref.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 ref1; const srcBasePath = config.basePath && r.basePath !== false ? config.basePath : ''; const isExternal = !((ref1 = r.destination) === null || ref1 === void 0 ? void 0 : ref1.startsWith('/')); const destBasePath = srcBasePath && !isExternal ? srcBasePath : ''; if (config.i18n && r.locale !== false) { var ref2; 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}` }); }); } r.source = `/:nextInternalLocale(${config.i18n.locales.map((locale)=>(0, _escapeRegexp).escapeStringRegexp(locale) ).join('|')})${r.source === '/' && !config.trailingSlash ? '' : r.source}`; if (r.destination && ((ref2 = r.destination) === null || ref2 === void 0 ? void 0 : ref2.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'); 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'); 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; } //# sourceMappingURL=load-custom-routes.js.map