2020-08-13 14:39:36 +02:00
import { ParsedUrlQuery } from 'querystring'
import { searchParamsToUrlQuery } from './querystring'
import { parseRelativeUrl } from './parse-relative-url'
2020-08-14 20:51:58 +02:00
import * as pathToRegexp from 'next/dist/compiled/path-to-regexp'
2020-08-13 14:39:36 +02:00
type Params = { [ param : string ] : any }
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
) {
let parsedDestination : {
query? : ParsedUrlQuery
protocol? : string
hostname? : string
port? : string
} & ReturnType < typeof parseRelativeUrl > = { } as any
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
2020-08-13 14:39:36 +02:00
if ( destination . startsWith ( '/' ) ) {
parsedDestination = parseRelativeUrl ( destination )
} else {
const {
pathname ,
searchParams ,
hash ,
hostname ,
port ,
protocol ,
search ,
href ,
} = new URL ( destination )
parsedDestination = {
pathname ,
2020-09-03 20:26:52 +02:00
query : searchParamsToUrlQuery ( searchParams ) ,
2020-08-13 14:39:36 +02:00
hash ,
protocol ,
hostname ,
port ,
search ,
href ,
}
}
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 ) ) {
let value = Array . isArray ( strOrArray ) ? strOrArray [ 0 ] : strOrArray
if ( value ) {
// the value needs to start with a forward-slash to be compiled
// correctly
2020-11-07 05:30:14 +01:00
value = compileNonPath ( value , params )
2020-08-13 14:39:36 +02:00
}
destQuery [ key ] = value
}
// 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 (
` To use a multi-match in the destination you must add \` * \` at the end of the param name to signify it should repeat. https://err.sh/vercel/next.js/invalid-multi-match `
)
}
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 ,
}
}