2020-04-21 14:21:41 +02:00
import { parse as parseUrl } from 'url'
2020-12-04 11:14:55 +01:00
import { NextConfig } from '../next-server/server/config'
import * as pathToRegexp from 'next/dist/compiled/path-to-regexp'
import escapeStringRegexp from 'next/dist/compiled/escape-string-regexp'
2020-01-14 19:28:48 +01:00
import {
PERMANENT_REDIRECT_STATUS ,
TEMPORARY_REDIRECT_STATUS ,
} from '../next-server/lib/constants'
2019-12-19 17:48:34 +01:00
2019-12-10 15:54:56 +01:00
export type Rewrite = {
source : string
destination : string
2020-07-12 21:03:49 +02:00
basePath? : false
2020-11-14 04:35:42 +01:00
locale? : false
2019-12-10 15:54:56 +01:00
}
2020-01-01 13:47:58 +01:00
export type Header = {
source : string
2020-07-12 21:03:49 +02:00
basePath? : false
2020-11-14 04:35:42 +01:00
locale? : false
2020-01-01 13:47:58 +01:00
headers : Array < { key : string ; value : string } >
}
2020-11-11 08:13:18 +01:00
// internal type used for validation (not user facing)
export type Redirect = Rewrite & {
statusCode? : number
permanent? : boolean
}
2020-11-02 22:47:59 +01:00
export const allowedStatusCodes = new Set ( [ 301 , 302 , 303 , 307 , 308 ] )
2019-12-10 15:54:56 +01:00
2020-11-04 23:18:44 +01:00
export function getRedirectStatus ( route : {
statusCode? : number
permanent? : boolean
} ) : number {
2020-01-14 19:28:48 +01:00
return (
route . statusCode ||
( route . permanent ? PERMANENT_REDIRECT_STATUS : TEMPORARY_REDIRECT_STATUS )
)
}
2020-07-08 20:45:53 +02:00
export function normalizeRouteRegex ( regex : string ) {
// clean up un-necessary escaping from regex.source which turns / into \\/
return regex . replace ( /\\\//g , '/' )
}
2020-05-19 10:59:03 +02:00
function checkRedirect (
route : Redirect
) : { invalidParts : string [ ] ; hadInvalidStatus : boolean } {
2020-01-01 13:47:58 +01:00
const invalidParts : string [ ] = [ ]
let hadInvalidStatus : boolean = false
if ( route . statusCode && ! allowedStatusCodes . has ( route . statusCode ) ) {
hadInvalidStatus = true
invalidParts . push ( ` \` statusCode \` is not undefined or valid statusCode ` )
}
2020-01-14 19:28:48 +01:00
if ( typeof route . permanent !== 'boolean' && ! route . statusCode ) {
invalidParts . push ( ` \` permanent \` is not set to \` true \` or \` false \` ` )
}
2020-01-01 13:47:58 +01:00
return {
invalidParts ,
hadInvalidStatus ,
}
}
2020-05-19 10:59:03 +02:00
function checkHeader ( route : Header ) : string [ ] {
2020-01-01 13:47:58 +01:00
const invalidParts : string [ ] = [ ]
if ( ! Array . isArray ( route . headers ) ) {
invalidParts . push ( '`headers` field must be an array' )
} 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
}
2020-04-21 14:21:41 +02:00
type ParseAttemptResult = {
error? : boolean
tokens? : pathToRegexp.Token [ ]
}
function tryParsePath ( route : string , handleUrl? : boolean ) : ParseAttemptResult {
const result : ParseAttemptResult = { }
let routePath = route
try {
if ( handleUrl ) {
const parsedDestination = parseUrl ( route , true )
2020-05-18 21:24:37 +02:00
routePath = ` ${ parsedDestination . pathname ! } ${
parsedDestination . hash || ''
} `
2020-04-21 14:21:41 +02:00
}
// Make sure we can parse the source properly
result . tokens = pathToRegexp . parse ( routePath )
pathToRegexp . tokensToRegexp ( result . tokens )
} catch ( err ) {
// If there is an error show our err.sh but still show original error or a formatted one if we can
const errMatches = err . message . match ( /at (\d{0,})/ )
if ( errMatches ) {
const position = parseInt ( errMatches [ 1 ] , 10 )
console . error (
` \ nError parsing \` ${ route } \` ` +
2020-05-27 23:51:11 +02:00
` https://err.sh/vercel/next.js/invalid-route-source \ n ` +
2020-04-21 14:21:41 +02:00
` Reason: ${ err . message } \ n \ n ` +
` ${ routePath } \ n ` +
` ${ new Array ( position ) . fill ( ' ' ) . join ( '' ) } ^ \ n `
)
} else {
console . error (
2020-05-27 23:51:11 +02:00
` \ nError parsing ${ route } https://err.sh/vercel/next.js/invalid-route-source ` ,
2020-04-21 14:21:41 +02:00
err
)
}
result . error = true
}
return result
}
2020-01-01 13:47:58 +01:00
export type RouteType = 'rewrite' | 'redirect' | 'header'
2020-06-09 22:16:23 +02:00
function checkCustomRoutes (
2020-01-01 13:47:58 +01:00
routes : Redirect [ ] | Header [ ] | Rewrite [ ] ,
type : RouteType
2019-12-10 15:54:56 +01:00
) : void {
2020-02-24 23:01:02 +01:00
if ( ! Array . isArray ( routes ) ) {
throw new Error (
` ${ type } s must return an array, received ${ typeof routes } . \ n ` +
` See here for more info: https://err.sh/next.js/routes-must-be-array `
)
}
2019-12-10 15:54:56 +01:00
let numInvalidRoutes = 0
let hadInvalidStatus = false
2020-01-01 13:47:58 +01:00
2019-12-10 15:54:56 +01:00
const isRedirect = type === 'redirect'
2020-01-01 13:47:58 +01:00
let allowedKeys : Set < string >
if ( type === 'rewrite' || isRedirect ) {
allowedKeys = new Set ( [
'source' ,
'destination' ,
2020-07-12 21:03:49 +02:00
'basePath' ,
2020-11-14 04:35:42 +01:00
'locale' ,
2020-01-14 19:28:48 +01:00
. . . ( isRedirect ? [ 'statusCode' , 'permanent' ] : [ ] ) ,
2020-01-01 13:47:58 +01:00
] )
} else {
2020-11-14 04:35:42 +01:00
allowedKeys = new Set ( [ 'source' , 'headers' , 'basePath' , 'locale' ] )
2020-01-01 13:47:58 +01:00
}
2019-12-10 15:54:56 +01:00
for ( const route of routes ) {
2020-01-23 18:08:25 +01:00
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
}
2020-08-04 19:50:09 +02:00
if (
type === 'rewrite' &&
( route as Rewrite ) . basePath === false &&
! (
( route as Rewrite ) . destination . startsWith ( 'http://' ) ||
( route as Rewrite ) . destination . startsWith ( 'https://' )
)
) {
console . error (
` The route ${
( route as Rewrite ) . source
} rewrites urls outside of the basePath . Please use a destination that starts with \ ` http:// \` or \` https:// \` https://err.sh/vercel/next.js/invalid-external-rewrite.md `
)
numInvalidRoutes ++
continue
}
2019-12-10 15:54:56 +01:00
const keys = Object . keys ( route )
2020-05-18 21:24:37 +02:00
const invalidKeys = keys . filter ( ( key ) = > ! allowedKeys . has ( key ) )
2020-01-01 13:47:58 +01:00
const invalidParts : string [ ] = [ ]
2019-12-10 15:54:56 +01:00
2020-07-12 21:03:49 +02:00
if ( typeof route . basePath !== 'undefined' && route . basePath !== false ) {
invalidParts . push ( '`basePath` must be undefined or false' )
}
2020-11-14 04:35:42 +01:00
if ( typeof route . locale !== 'undefined' && route . locale !== false ) {
2020-11-19 17:04:55 +01:00
invalidParts . push ( '`locale` must be undefined or false' )
2020-11-14 04:35:42 +01:00
}
2019-12-10 15:54:56 +01:00
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 /' )
}
2020-01-01 13:47:58 +01:00
if ( type === 'header' ) {
invalidParts . push ( . . . checkHeader ( route as Header ) )
} else {
let _route = route as Rewrite | Redirect
if ( ! _route . destination ) {
invalidParts . push ( '`destination` is missing' )
} else if ( typeof _route . destination !== 'string' ) {
invalidParts . push ( '`destination` is not a string' )
2020-02-04 20:08:03 +01:00
} else if (
type === 'rewrite' &&
! _route . destination . match ( /^(\/|https:\/\/|http:\/\/)/ )
) {
invalidParts . push (
'`destination` does not start with `/`, `http://`, or `https://`'
)
2020-01-01 13:47:58 +01:00
}
2019-12-10 15:54:56 +01:00
}
2020-01-01 13:47:58 +01:00
if ( type === 'redirect' ) {
const result = checkRedirect ( route as Redirect )
2020-01-14 19:28:48 +01:00
hadInvalidStatus = hadInvalidStatus || result . hadInvalidStatus
2020-01-01 13:47:58 +01:00
invalidParts . push ( . . . result . invalidParts )
2019-12-10 15:54:56 +01:00
}
2020-03-10 21:09:35 +01:00
let sourceTokens : pathToRegexp.Token [ ] | undefined
2020-01-22 11:16:13 +01:00
if ( typeof route . source === 'string' && route . source . startsWith ( '/' ) ) {
2020-01-01 13:47:58 +01:00
// only show parse error if we didn't already show error
// for not being a string
2020-04-21 14:21:41 +02:00
const { tokens , error } = tryParsePath ( route . source )
if ( error ) {
2020-01-22 11:16:13 +01:00
invalidParts . push ( '`source` parse failed' )
2020-01-01 13:47:58 +01:00
}
2020-04-21 14:21:41 +02:00
sourceTokens = tokens
2019-12-19 17:48:34 +01:00
}
2020-03-10 21:09:35 +01:00
// 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 as Rewrite ) . destination === 'string' ) {
if (
( route as Rewrite ) . destination . startsWith ( '/' ) &&
Array . isArray ( sourceTokens )
) {
const unnamedInDest = new Set ( )
for ( const token of sourceTokens ) {
if ( typeof token === 'object' && typeof token . name === 'number' ) {
2020-04-21 14:21:41 +02:00
const unnamedIndex = new RegExp ( ` : ${ token . name } (?! \\ d) ` )
if ( ( route as Rewrite ) . destination . match ( unnamedIndex ) ) {
unnamedInDest . add ( ` : ${ token . name } ` )
2020-03-10 21:09:35 +01:00
}
}
}
if ( unnamedInDest . size > 0 ) {
invalidParts . push (
` \` destination \` has unnamed params ${ [ . . . unnamedInDest ] . join (
', '
) } `
)
2020-04-21 14:21:41 +02:00
} else {
const {
tokens : destTokens ,
error : destinationParseFailed ,
} = tryParsePath ( ( route as Rewrite ) . destination , true )
if ( destinationParseFailed ) {
invalidParts . push ( '`destination` parse failed' )
} else {
const sourceSegments = new Set (
sourceTokens
2020-05-18 21:24:37 +02:00
. map ( ( item ) = > typeof item === 'object' && item . name )
2020-04-21 14:21:41 +02:00
. filter ( Boolean )
)
const invalidDestSegments = new Set ( )
for ( const token of destTokens ! ) {
if (
typeof token === 'object' &&
! sourceSegments . has ( token . name )
) {
invalidDestSegments . add ( token . name )
}
}
if ( invalidDestSegments . size ) {
invalidParts . push (
` \` destination \` has segments not in \` source \` ( ${ [
. . . invalidDestSegments ,
] . join ( ', ' ) } ) `
)
}
}
2020-03-10 21:09:35 +01:00
}
}
}
2019-12-10 15:54:56 +01:00
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 ) } `
)
numInvalidRoutes ++
}
}
if ( numInvalidRoutes > 0 ) {
if ( hadInvalidStatus ) {
console . error (
` \ nValid redirect statusCode values are ${ [ . . . allowedStatusCodes ] . join (
', '
) } `
)
}
console . error ( )
throw new Error ( ` Invalid ${ type } ${ numInvalidRoutes === 1 ? '' : 's' } found ` )
}
}
2020-06-09 22:16:23 +02:00
export interface CustomRoutes {
headers : Header [ ]
rewrites : Rewrite [ ]
redirects : Redirect [ ]
}
2020-12-04 11:14:55 +01:00
function processRoutes < T > (
routes : T ,
config : NextConfig ,
type : 'redirect' | 'rewrite' | 'header'
) : T {
const _routes = ( routes as any ) as Array < {
source : string
locale? : false
basePath? : false
destination? : string
} >
const newRoutes : typeof _routes = [ ]
const defaultLocales : Array < {
locale : string
base : string
} > = [ ]
if ( config . i18n && type === 'redirect' ) {
for ( const item of 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 ) {
const srcBasePath =
config . basePath && r . basePath !== false ? config . basePath : ''
const isExternal = ! r . destination ? . startsWith ( '/' )
const destBasePath = srcBasePath && ! isExternal ? srcBasePath : ''
if ( config . i18n && r . locale !== false ) {
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 : string ) = > escapeStringRegexp ( locale ) )
2020-12-28 19:21:28 +01:00
. join ( '|' ) } ) $ {
r . source === '/' && ! config . trailingSlash ? '' : r . source
} `
2020-12-04 11:14:55 +01:00
if ( r . destination && r . destination ? . startsWith ( '/' ) ) {
2020-12-22 18:12:53 +01:00
r . destination = ` /:nextInternalLocale ${
r . destination === '/' && ! config . trailingSlash ? '' : r . destination
} `
2020-12-04 11:14:55 +01:00
}
}
r . source = ` ${ srcBasePath } ${ r . source } `
if ( r . destination ) {
r . destination = ` ${ destBasePath } ${ r . destination } `
}
newRoutes . push ( r )
}
return ( newRoutes as any ) as T
}
async function loadRedirects ( config : NextConfig ) {
2020-06-27 11:18:18 +02:00
if ( typeof config . redirects !== 'function' ) {
2020-06-09 22:16:23 +02:00
return [ ]
}
2020-12-04 11:14:55 +01:00
let redirects = await config . redirects ( )
checkCustomRoutes ( redirects , 'redirect' )
return processRoutes ( redirects , config , 'redirect' )
2020-06-09 22:16:23 +02:00
}
2020-12-04 11:14:55 +01:00
async function loadRewrites ( config : NextConfig ) {
2020-06-27 11:18:18 +02:00
if ( typeof config . rewrites !== 'function' ) {
2020-06-09 22:16:23 +02:00
return [ ]
}
2020-12-04 11:14:55 +01:00
let rewrites = await config . rewrites ( )
checkCustomRoutes ( rewrites , 'rewrite' )
return processRoutes ( rewrites , config , 'rewrite' )
2020-06-09 22:16:23 +02:00
}
2020-12-04 11:14:55 +01:00
async function loadHeaders ( config : NextConfig ) {
2020-06-27 11:18:18 +02:00
if ( typeof config . headers !== 'function' ) {
2020-06-09 22:16:23 +02:00
return [ ]
}
2020-12-04 11:14:55 +01:00
let headers = await config . headers ( )
checkCustomRoutes ( headers , 'header' )
return processRoutes ( headers , config , 'header' )
2020-06-09 22:16:23 +02:00
}
export default async function loadCustomRoutes (
2020-12-04 11:14:55 +01:00
config : NextConfig
2020-06-09 22:16:23 +02:00
) : Promise < CustomRoutes > {
const [ headers , rewrites , redirects ] = await Promise . all ( [
loadHeaders ( config ) ,
loadRewrites ( config ) ,
loadRedirects ( config ) ,
] )
2020-07-20 18:16:59 +02:00
if ( config . trailingSlash ) {
2020-07-13 16:59:40 +02:00
redirects . unshift (
{
2020-11-20 22:17:58 +01:00
source : '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/' ,
2020-07-20 20:24:43 +02:00
destination : '/:file' ,
2020-07-13 16:59:40 +02:00
permanent : true ,
2020-11-14 04:35:42 +01:00
locale : config.i18n ? false : undefined ,
2020-12-07 18:36:46 +01:00
internal : true ,
2020-12-04 11:14:55 +01:00
} as Redirect ,
2020-07-13 16:59:40 +02:00
{
2020-11-20 22:17:58 +01:00
source : '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)' ,
2020-07-20 20:24:43 +02:00
destination : '/:notfile/' ,
2020-07-13 16:59:40 +02:00
permanent : true ,
2020-11-14 04:35:42 +01:00
locale : config.i18n ? false : undefined ,
2020-12-07 18:36:46 +01:00
internal : true ,
2020-12-04 11:14:55 +01:00
} as Redirect
2020-07-13 16:59:40 +02:00
)
if ( config . basePath ) {
redirects . unshift ( {
source : config.basePath ,
destination : config.basePath + '/' ,
permanent : true ,
basePath : false ,
2020-11-14 04:35:42 +01:00
locale : config.i18n ? false : undefined ,
2020-12-07 18:36:46 +01:00
internal : true ,
2020-12-04 11:14:55 +01:00
} as Redirect )
2020-07-13 16:59:40 +02:00
}
} else {
redirects . unshift ( {
source : '/:path+/' ,
destination : '/:path+' ,
permanent : true ,
2020-11-14 04:35:42 +01:00
locale : config.i18n ? false : undefined ,
2020-12-07 18:36:46 +01:00
internal : true ,
2020-12-04 11:14:55 +01:00
} as Redirect )
2020-07-13 16:59:40 +02:00
if ( config . basePath ) {
redirects . unshift ( {
source : config.basePath + '/' ,
destination : config.basePath ,
permanent : true ,
basePath : false ,
2020-11-14 04:35:42 +01:00
locale : config.i18n ? false : undefined ,
2020-12-07 18:36:46 +01:00
internal : true ,
2020-12-04 11:14:55 +01:00
} as Redirect )
2020-07-13 16:59:40 +02:00
}
}
2020-06-23 13:38:49 +02:00
2020-06-09 22:16:23 +02:00
return {
headers ,
rewrites ,
redirects ,
}
}