@@ -4,6 +4,8 @@ import type { Context } from '@netlify/edge-functions'
44
55import type { NetlifyAdapterContext } from '../build/types.js'
66
7+ export type RoutingPhase = 'entry' | 'filesystem' | 'rewrite'
8+
79type RoutingRuleBase = {
810 /**
911 * Human readable description of the rule (for debugging purposes only)
@@ -41,6 +43,8 @@ export type RoutingRuleRewrite = RoutingRuleBase & {
4143 destination : string
4244 /** Forced status code for response, if not defined rewrite response status code will be used */
4345 statusCode ?: 200 | 404 | 500
46+ /** Phases to re-run after matching this rewrite */
47+ rerunRoutingPhases ?: RoutingPhase [ ]
4448 }
4549}
4650
@@ -50,8 +54,6 @@ export type RoutingRuleMatchPrimitive = RoutingRuleBase & {
5054 }
5155}
5256
53- export type RoutingPhase = 'filesystem' | 'rewrite'
54-
5557export type RoutingPhaseRule = RoutingRuleBase & {
5658 routingPhase : RoutingPhase
5759}
@@ -62,62 +64,73 @@ export type RoutingRule =
6264 | RoutingPhaseRule
6365 | RoutingRuleMatchPrimitive
6466
65- export function testRedirectRewriteRule ( rule : RoutingRuleRedirect , request : Request ) {
66- const sourceRegexp = new RegExp ( rule . match . path )
67- const { pathname } = new URL ( request . url )
68- if ( sourceRegexp . test ( pathname ) ) {
69- const replaced = pathname . replace ( sourceRegexp , rule . apply . destination )
70- return { matched : true , replaced }
67+ function selectRoutingPhasesRules ( routingRules : RoutingRule [ ] , phases : RoutingPhase [ ] ) {
68+ const selectedRules : RoutingRule [ ] = [ ]
69+ let currentPhase : RoutingPhase | undefined
70+ for ( const rule of routingRules ) {
71+ if ( 'routingPhase' in rule ) {
72+ currentPhase = rule . routingPhase
73+ } else if ( currentPhase && phases . includes ( currentPhase ) ) {
74+ selectedRules . push ( rule )
75+ }
7176 }
72- return { matched : false }
77+
78+ return selectedRules
7379}
7480
7581let requestCounter = 0
7682
77- export async function runNextRouting (
83+ function normalizeIndex ( request : Request , prefix : string ) {
84+ const currentURL = new URL ( request . url )
85+ const { pathname } = currentURL
86+ if ( pathname === '/' ) {
87+ const destURL = new URL ( '/index' , currentURL )
88+ console . log ( prefix , 'Normalizing "/" to "/index" for routing purposes' )
89+ return new Request ( destURL , request )
90+ }
91+ return request
92+ }
93+
94+ // eslint-disable-next-line max-params
95+ async function match (
7896 request : Request ,
7997 context : Context ,
8098 routingRules : RoutingRule [ ] ,
8199 outputs : NetlifyAdapterContext [ 'preparedOutputs' ] ,
100+ prefix : string ,
82101) {
83- if ( request . headers . has ( 'x-ntl-routing' ) ) {
84- // don't route multiple times for same request
85- return
86- }
87-
88- const prefix = `[${
89- request . headers . get ( 'x-nf-request-id' ) ??
90- // for ntl serve, we use a combination of timestamp and pid to have a unique id per request as we don't have x-nf-request-id header then
91- // eslint-disable-next-line no-plusplus
92- `${ Date . now ( ) } - #${ process . pid } :${ ++ requestCounter } `
93- } ]`
94-
95- console . log ( prefix , 'Incoming request for routing:' , request . url )
96-
97- let currentRequest = new Request ( request )
98- currentRequest . headers . set ( 'x-ntl-routing' , '1' )
102+ let currentRequest = normalizeIndex ( request , prefix )
99103 let maybeResponse : Response | undefined
100104
105+ const currentURL = new URL ( currentRequest . url )
106+ let { pathname } = currentURL
107+
101108 for ( const rule of routingRules ) {
102109 console . log ( prefix , 'Evaluating rule:' , rule . description ?? JSON . stringify ( rule ) )
103110 if ( 'match' in rule ) {
104- const currentURL = new URL ( currentRequest . url )
105- const { pathname } = currentURL
106-
107111 if ( 'type' in rule . match ) {
108- const pathnameToMatch = pathname === '/' ? '/index' : pathname
109-
110112 if ( rule . match . type === 'static-asset-or-function' ) {
111- let matchedType : 'static-asset' | 'function' | null = null
113+ let matchedType : 'static-asset' | 'function' | 'static-asset-alias' | null = null
112114
113- if ( outputs . staticAssets . includes ( pathnameToMatch ) ) {
115+ // below assumes no overlap between static assets (files and aliases) and functions
116+ if ( outputs . staticAssets . includes ( pathname ) ) {
114117 matchedType = 'static-asset'
115- } else if ( outputs . endpoints . includes ( pathnameToMatch ) ) {
118+ } else if ( outputs . endpoints . includes ( pathname ) ) {
116119 matchedType = 'function'
120+ } else {
121+ const staticAlias = outputs . staticAssetsAliases [ pathname ]
122+ if ( staticAlias ) {
123+ matchedType = 'static-asset-alias'
124+ currentRequest = new Request ( new URL ( staticAlias , currentRequest . url ) , currentRequest )
125+ pathname = staticAlias
126+ }
117127 }
118128
119129 if ( matchedType ) {
120- console . log ( prefix , `Matched static asset or function (${ matchedType } ):` )
130+ console . log (
131+ prefix ,
132+ `Matched static asset or function (${ matchedType } ): ${ pathname } -> ${ currentRequest . url } ` ,
133+ )
121134 maybeResponse = await context . next ( currentRequest )
122135 }
123136 } else if ( rule . match . type === 'image-cdn' && pathname . startsWith ( '/.netlify/image/' ) ) {
@@ -146,9 +159,18 @@ export async function runNextRouting(
146159 const replaced = pathname . replace ( sourceRegexp , rule . apply . destination )
147160
148161 if ( rule . apply . type === 'rewrite' ) {
149- console . log ( prefix , `Rewriting ${ pathname } to ${ replaced } ` )
150162 const destURL = new URL ( replaced , currentURL )
151163 currentRequest = new Request ( destURL , currentRequest )
164+
165+ if ( rule . apply . rerunRoutingPhases ) {
166+ maybeResponse = await match (
167+ currentRequest ,
168+ context ,
169+ selectRoutingPhasesRules ( routingRules , rule . apply . rerunRoutingPhases ) ,
170+ outputs ,
171+ prefix ,
172+ )
173+ }
152174 } else {
153175 console . log ( prefix , `Redirecting ${ pathname } to ${ replaced } ` )
154176 maybeResponse = new Response ( null , {
@@ -163,10 +185,44 @@ export async function runNextRouting(
163185 }
164186
165187 if ( maybeResponse ) {
166- // for debugging add log prefixes to response headers to make it easy to find logs for a given request
167- maybeResponse . headers . set ( 'x-ntl-log-prefix' , prefix )
168- console . log ( prefix , 'Serving response' , maybeResponse . status )
169188 return maybeResponse
170189 }
171190 }
172191}
192+
193+ export async function runNextRouting (
194+ request : Request ,
195+ context : Context ,
196+ routingRules : RoutingRule [ ] ,
197+ outputs : NetlifyAdapterContext [ 'preparedOutputs' ] ,
198+ ) {
199+ if ( request . headers . has ( 'x-ntl-routing' ) ) {
200+ // don't route multiple times for same request
201+ return
202+ }
203+
204+ const prefix = `[${
205+ request . headers . get ( 'x-nf-request-id' ) ??
206+ // for ntl serve, we use a combination of timestamp and pid to have a unique id per request as we don't have x-nf-request-id header then
207+ // eslint-disable-next-line no-plusplus
208+ `${ Date . now ( ) } - #${ process . pid } :${ ++ requestCounter } `
209+ } ]`
210+
211+ console . log ( prefix , 'Incoming request for routing:' , request . url )
212+
213+ const currentRequest = new Request ( request )
214+ currentRequest . headers . set ( 'x-ntl-routing' , '1' )
215+
216+ let maybeResponse = await match ( currentRequest , context , routingRules , outputs , prefix )
217+
218+ if ( ! maybeResponse ) {
219+ console . log ( prefix , 'No route matched - 404ing' )
220+ maybeResponse = new Response ( 'Not Found' , { status : 404 } )
221+ }
222+
223+ // for debugging add log prefixes to response headers to make it easy to find logs for a given request
224+ maybeResponse . headers . set ( 'x-ntl-log-prefix' , prefix )
225+ console . log ( prefix , 'Serving response' , maybeResponse . status )
226+
227+ return maybeResponse
228+ }
0 commit comments