2021-07-21 18:12:33 +02:00
import type { IncomingMessage } from 'http'
import type { ParsedUrlQuery } from 'querystring'
import { parseUrl } from './parse-url'
2020-08-14 20:51:58 +02:00
import * as pathToRegexp from 'next/dist/compiled/path-to-regexp'
2021-07-21 18:12:33 +02:00
import type { RouteHas } from '../../../../lib/load-custom-routes'
2020-08-13 14:39:36 +02:00
type Params = { [ param : string ] : any }
2021-03-24 17:50:16 +01:00
// ensure only a-zA-Z are used for param names for proper interpolating
// with path-to-regexp
2021-04-01 11:15:28 +02:00
export const getSafeParamName = ( paramName : string ) = > {
2021-03-24 17:50:16 +01:00
let newParamName = ''
for ( let i = 0 ; i < paramName . length ; i ++ ) {
const charCode = paramName . charCodeAt ( i )
if (
( charCode > 64 && charCode < 91 ) || // A-Z
( charCode > 96 && charCode < 123 ) // a-z
) {
newParamName += paramName [ i ]
}
}
return newParamName
}
export function matchHas (
req : IncomingMessage ,
has : RouteHas [ ] ,
query : Params
) : false | Params {
const params : Params = { }
2021-06-17 17:52:11 +02:00
2021-03-24 17:50:16 +01:00
const allMatch = has . every ( ( hasItem ) = > {
let value : undefined | string
let key = hasItem . key
switch ( hasItem . type ) {
case 'header' : {
key = key ! . toLowerCase ( )
value = req . headers [ key ] as string
break
}
case 'cookie' : {
value = ( req as any ) . cookies [ hasItem . key ]
break
}
case 'query' : {
value = query [ key ! ]
break
}
case 'host' : {
const { host } = req ? . headers || { }
// remove port from host if present
const hostname = host ? . split ( ':' ) [ 0 ] . toLowerCase ( )
value = hostname
break
}
default : {
break
}
}
if ( ! hasItem . value && value ) {
params [ getSafeParamName ( key ! ) ] = value
return true
} else if ( value ) {
const matcher = new RegExp ( ` ^ ${ hasItem . value } $ ` )
const matches = value . match ( matcher )
if ( matches ) {
if ( matches . groups ) {
Object . keys ( matches . groups ) . forEach ( ( groupKey ) = > {
2021-04-13 14:34:51 +02:00
params [ groupKey ] = matches . groups ! [ groupKey ]
2021-03-24 17:50:16 +01:00
} )
2021-04-13 14:34:51 +02:00
} else if ( hasItem . type === 'host' && matches [ 0 ] ) {
params . host = matches [ 0 ]
2021-03-24 17:50:16 +01:00
}
return true
}
}
return false
} )
if ( allMatch ) {
return params
}
return false
}
2020-11-07 05:30:14 +01:00
export function compileNonPath ( value : string , params : Params ) : string {
if ( ! value . includes ( ':' ) ) {
return value
}
for ( const key of Object . keys ( params ) ) {
if ( value . includes ( ` : ${ key } ` ) ) {
value = value
. replace (
new RegExp ( ` : ${ key } \\ * ` , 'g' ) ,
` : ${ key } --ESCAPED_PARAM_ASTERISKS `
)
. replace (
new RegExp ( ` : ${ key } \\ ? ` , 'g' ) ,
` : ${ key } --ESCAPED_PARAM_QUESTION `
)
. replace ( new RegExp ( ` : ${ key } \\ + ` , 'g' ) , ` : ${ key } --ESCAPED_PARAM_PLUS ` )
. replace (
new RegExp ( ` : ${ key } (?! \\ w) ` , 'g' ) ,
` --ESCAPED_PARAM_COLON ${ key } `
)
}
}
value = value
. replace ( /(:|\*|\?|\+|\(|\)|\{|\})/g , '\\$1' )
. replace ( /--ESCAPED_PARAM_PLUS/g , '+' )
. replace ( /--ESCAPED_PARAM_COLON/g , ':' )
. replace ( /--ESCAPED_PARAM_QUESTION/g , '?' )
. replace ( /--ESCAPED_PARAM_ASTERISKS/g , '*' )
// the value needs to start with a forward-slash to be compiled
// correctly
return pathToRegexp
. compile ( ` / ${ value } ` , { validate : false } ) ( params )
. substr ( 1 )
}
2020-08-13 14:39:36 +02:00
export default function prepareDestination (
destination : string ,
params : Params ,
query : ParsedUrlQuery ,
2020-12-04 11:14:55 +01:00
appendParamsToQuery : boolean
2020-08-13 14:39:36 +02:00
) {
2020-10-29 18:48:54 +01:00
// clone query so we don't modify the original
query = Object . assign ( { } , query )
2020-11-14 04:35:42 +01:00
const hadLocale = query . __nextLocale
2020-10-29 18:48:54 +01:00
delete query . __nextLocale
2020-11-11 03:09:45 +01:00
delete query . __nextDefaultLocale
2020-10-29 18:48:54 +01:00
2021-07-21 18:12:33 +02:00
const parsedDestination = parseUrl ( destination )
2020-08-13 14:39:36 +02:00
const destQuery = parsedDestination . query
2020-08-14 20:51:58 +02:00
const destPath = ` ${ parsedDestination . pathname ! } ${
parsedDestination . hash || ''
} `
const destPathParamKeys : pathToRegexp.Key [ ] = [ ]
pathToRegexp . pathToRegexp ( destPath , destPathParamKeys )
2020-08-13 14:39:36 +02:00
2020-08-14 20:51:58 +02:00
const destPathParams = destPathParamKeys . map ( ( key ) = > key . name )
let destinationCompiler = pathToRegexp . compile (
destPath ,
2020-08-13 14:39:36 +02:00
// we don't validate while compiling the destination since we should
// have already validated before we got to this point and validating
// breaks compiling destinations with named pattern params from the source
// e.g. /something:hello(.*) -> /another/:hello is broken with validation
// since compile validation is meant for reversing and not for inserting
// params from a separate path-regex into another
{ validate : false }
)
let newUrl
// update any params in query values
for ( const [ key , strOrArray ] of Object . entries ( destQuery ) ) {
2021-07-06 23:20:53 +02:00
// the value needs to start with a forward-slash to be compiled
// correctly
if ( Array . isArray ( strOrArray ) ) {
destQuery [ key ] = strOrArray . map ( ( value ) = > compileNonPath ( value , params ) )
} else {
destQuery [ key ] = compileNonPath ( strOrArray , params )
2020-08-13 14:39:36 +02:00
}
}
// add path params to query if it's not a redirect and not
2020-08-14 20:51:58 +02:00
// already defined in destination query or path
2020-11-14 04:35:42 +01:00
let paramKeys = Object . keys ( params )
// remove internal param for i18n
if ( hadLocale ) {
paramKeys = paramKeys . filter ( ( name ) = > name !== 'nextInternalLocale' )
}
2020-08-14 20:51:58 +02:00
if (
appendParamsToQuery &&
! paramKeys . some ( ( key ) = > destPathParams . includes ( key ) )
) {
for ( const key of paramKeys ) {
if ( ! ( key in destQuery ) ) {
destQuery [ key ] = params [ key ]
2020-08-13 14:39:36 +02:00
}
}
}
try {
2020-12-04 11:14:55 +01:00
newUrl = destinationCompiler ( params )
2020-08-13 14:39:36 +02:00
const [ pathname , hash ] = newUrl . split ( '#' )
parsedDestination . pathname = pathname
parsedDestination . hash = ` ${ hash ? '#' : '' } ${ hash || '' } `
2020-11-14 04:35:42 +01:00
delete ( parsedDestination as any ) . search
2020-08-13 14:39:36 +02:00
} catch ( err ) {
if ( err . message . match ( /Expected .*? to not repeat, but got an array/ ) ) {
throw new Error (
2021-03-29 10:25:00 +02:00
` To use a multi-match in the destination you must add \` * \` at the end of the param name to signify it should repeat. https://nextjs.org/docs/messages/invalid-multi-match `
2020-08-13 14:39:36 +02:00
)
}
throw err
}
// Query merge order lowest priority to highest
// 1. initial URL query values
// 2. path segment values
// 3. destination specified query values
parsedDestination . query = {
. . . query ,
. . . parsedDestination . query ,
}
return {
newUrl ,
parsedDestination ,
}
}