11import { cp , writeFile } from 'node:fs/promises'
22import { join } from 'node:path/posix'
3- import { format as formatUrl , parse as parseUrl } from 'node:url'
43
54import { glob } from 'fast-glob'
6- import {
7- pathToRegexp ,
8- compile as pathToRegexpCompile ,
9- type Key as PathToRegexpKey ,
10- } from 'path-to-regexp'
115
126import type { RoutingRule , RoutingRuleRedirect , RoutingRuleRewrite } from '../run/routing.js'
137
@@ -19,144 +13,21 @@ import {
1913} from './constants.js'
2014import type { NetlifyAdapterContext , OnBuildCompleteContext } from './types.js'
2115
22- const UN_NAMED_SEGMENT = '__UN_NAMED_SEGMENT__'
23-
24- // https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L273
25- export function sourceToRegex ( source : string ) {
26- const keys : PathToRegexpKey [ ] = [ ]
27- const regexp = pathToRegexp ( source , keys , {
28- strict : true ,
29- sensitive : true ,
30- delimiter : '/' ,
31- } )
32-
33- return {
34- sourceRegexString : regexp . source ,
35- segments : keys
36- . map ( ( key ) => key . name )
37- . map ( ( keyName ) => {
38- if ( typeof keyName !== 'string' ) {
39- return UN_NAMED_SEGMENT
40- }
41- return keyName
42- } ) ,
43- }
44- }
45-
46- // https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L345
47- const escapeSegment = ( str : string , segmentName : string ) =>
48- str . replace ( new RegExp ( `:${ segmentName } ` , 'g' ) , `__esc_colon_${ segmentName } ` )
49-
50- // https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L348
51- const unescapeSegments = ( str : string ) => str . replace ( / _ _ e s c _ c o l o n _ / gi, ':' )
52-
53- // https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L464
54- function safelyCompile (
55- val : string ,
56- indexes : { [ k : string ] : string } ,
57- attemptDirectCompile ?: boolean ,
58- ) : string {
59- let value = val
60- if ( ! value ) {
61- return value
62- }
63-
64- if ( attemptDirectCompile ) {
65- try {
66- // Attempt compiling normally with path-to-regexp first and fall back
67- // to safely compiling to handle edge cases if path-to-regexp compile
68- // fails
69- return pathToRegexpCompile ( value , { validate : false } ) ( indexes )
70- } catch {
71- // non-fatal, we continue to safely compile
72- }
73- }
74-
75- for ( const key of Object . keys ( indexes ) ) {
76- if ( value . includes ( `:${ key } ` ) ) {
77- value = value
78- . replace ( new RegExp ( `:${ key } \\*` , 'g' ) , `:${ key } --ESCAPED_PARAM_ASTERISK` )
79- . replace ( new RegExp ( `:${ key } \\?` , 'g' ) , `:${ key } --ESCAPED_PARAM_QUESTION` )
80- . replace ( new RegExp ( `:${ key } \\+` , 'g' ) , `:${ key } --ESCAPED_PARAM_PLUS` )
81- . replace ( new RegExp ( `:${ key } (?!\\w)` , 'g' ) , `--ESCAPED_PARAM_COLON${ key } ` )
82- }
83- }
84- value = value
85- // eslint-disable-next-line unicorn/better-regex
86- . replace ( / ( : | \* | \? | \+ | \( | \) | \{ | \} ) / g, '\\$1' )
87- . replace ( / - - E S C A P E D _ P A R A M _ P L U S / g, '+' )
88- . replace ( / - - E S C A P E D _ P A R A M _ C O L O N / g, ':' )
89- . replace ( / - - E S C A P E D _ P A R A M _ Q U E S T I O N / g, '?' )
90- . replace ( / - - E S C A P E D _ P A R A M _ A S T E R I S K / g, '*' )
91-
92- // the value needs to start with a forward-slash to be compiled
93- // correctly
94- return pathToRegexpCompile ( `/${ value } ` , { validate : false } ) ( indexes ) . slice ( 1 )
95- }
96-
97- // https://github.com/vercel/vercel/blob/8beae7035bf0d3e5cfc1df337b83fbbe530c4d9b/packages/routing-utils/src/superstatic.ts#L350
98- export function destinationToReplacementString ( destination : string , segments : string [ ] ) {
99- // convert /path/:id/route to /path/$1/route
100- // convert /path/:id+ to /path/$1
101-
102- let escapedDestination = destination
103-
104- const indexes : { [ k : string ] : string } = { }
105-
106- segments . forEach ( ( name , index ) => {
107- indexes [ name ] = `$${ index + 1 } `
108- escapedDestination = escapeSegment ( escapedDestination , name )
109- } )
110-
111- const parsedDestination = parseUrl ( escapedDestination , true )
112- delete ( parsedDestination as any ) . href
113- delete ( parsedDestination as any ) . path
114- delete ( parsedDestination as any ) . search
115- delete ( parsedDestination as any ) . host
116- let { pathname, ...rest } = parsedDestination
117- pathname = unescapeSegments ( pathname || '' )
118-
119- const pathnameKeys : PathToRegexpKey [ ] = [ ]
120-
121- try {
122- pathToRegexp ( pathname , pathnameKeys )
123- } catch {
124- // this is not fatal so don't error when failing to parse the
125- // params from the destination
126- }
127-
128- pathname = safelyCompile ( pathname , indexes , true )
129-
130- const finalDestination = formatUrl ( {
131- ...rest ,
132- // hostname,
133- pathname,
134- // query,
135- // hash,
136- } )
137- // url.format() escapes the dollar sign but it must be preserved for now-proxy
138- return finalDestination . replace ( / % 2 4 / g, '$' )
139- }
140-
14116export function convertRedirectToRoutingRule (
14217 redirect : Pick <
14318 OnBuildCompleteContext [ 'routes' ] [ 'redirects' ] [ number ] ,
144- 'source ' | 'destination' | 'priority'
19+ 'sourceRegex ' | 'destination' | 'priority'
14520 > ,
14621 description : string ,
14722) : RoutingRuleRedirect {
148- const { sourceRegexString, segments } = sourceToRegex ( redirect . source )
149-
150- const convertedDestination = destinationToReplacementString ( redirect . destination , segments )
151-
15223 return {
15324 description,
15425 match : {
155- path : sourceRegexString ,
26+ path : redirect . sourceRegex ,
15627 } ,
15728 apply : {
15829 type : 'redirect' ,
159- destination : convertedDestination ,
30+ destination : redirect . destination ,
16031 } ,
16132 } satisfies RoutingRuleRedirect
16233}
0 commit comments