import pathToRegExp from 'path-to-regexp' ; import { Context } from './Context' ; import { pathJoin } from './path' ; export type Handler < ContextData = any , Params = any > = ( ctx : Context < ContextData , Params > , next ? : ( ) => Promise < void > ) => any ; export type Middleware < ContextData = any , Params = any > = ( ctx : Context < ContextData , Params > , next : ( ) => Promise < void > ) => any ; export type Method = 'ALL' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' ; export interface Route { readonly original : string ; readonly pattern : readonly [ RegExp , pathToRegExp . Token [ ] ] ; readonly handler : Handler | Middleware ; readonly method : Method ; } export interface RouteMatch { params : any ; route : Route ; } type RouteRequest = Pick < Request , 'url' | 'method' > ; export interface RouteTagResult < ContextData , Params > { /**
     * Handle a request
     */ handle : ( handler : Handler < ContextData , Params > ) => RouteTagResult < ContextData , Params > ; /**
     * Mount middleware
     */ use : ( handler : Middleware < ContextData , Params > | Router ) => RouteTagResult < ContextData , Params > ; /**
     * Get back the original router instance
     */ router : ( ) => Router < ContextData > ; } export class Router < ContextData = any > { public routes : Route [ ] = [ ] ; all = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'ALL' , strings , paramNames ) ; get = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'GET' , strings , paramNames ) ; post = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'POST' , strings , paramNames ) ; put = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'PUT' , strings , paramNames ) ; patch = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'PATCH' , strings , paramNames ) ; delete = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'DELETE' , strings , paramNames ) ; head = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'HEAD' , strings , paramNames ) ; options = < A extends string , T extends { [ K in A ] : string ; } > ( strings , ... paramNames ) => this . methodResult < A , T > ( 'OPTIONS' , strings , paramNames ) ; private methodResult = < Param extends string , Vars extends { [ K in Param ] : string ; } > ( method , strings , paramNames ) => { let original = strings . reduce ( ( result , str , i ) => { const paramString = ( ( paramNames as any ) [ i ] && `:${ ( paramNames as any ) [ i ] }` ) || '' ; return `${ result }${ str }${ paramString }` ; } , '' ) ; if ( original . endsWith ( '/' ) ) { original = original . substring ( 0 , original . length - 1 ) ; } const pattern = [ pathToRegExp ( original ) , pathToRegExp . parse ( original ) ] as const ; const result = { use : ( routerOrHandler ) => { if ( routerOrHandler instanceof Router ) { const router = routerOrHandler ; router . routes . forEach ( ( route ) => { const routeOriginal = pathJoin ( original , route . original ) ; this . routes . push ( { original : routeOriginal , pattern : [ pathToRegExp ( routeOriginal ) , pathToRegExp . parse ( routeOriginal ) ] , handler : route . handler , method : route . method , } ) ; } ) ; } else { const handler = routerOrHandler ; this . routes . push ( { original , pattern , handler , method } ) ; } return result ; } , handle : ( handler ) => { this . routes . push ( { original , pattern , handler , method } ) ; return result ; } , router : ( ) => this , } ; return result ; } ; getMatchingRoutesForURLAndMethod ( url , targetMethod ) { return this . routes . reduce ( ( result , route ) => { const { pattern : [ pattern , routeTokens ] , method , original , } = route ; if ( method !== 'ALL' && method !== targetMethod ) return result ; const patternResult = pattern . exec ( original . startsWith ( 'http' ) ? url . href : url . pathname ) ; if ( ! patternResult ) return result ; const params = { } ; let patternResultIndex = 1 ; for ( let i = 0 ; i < routeTokens . length ; i ++ ) { if ( typeof routeTokens [ i ] === 'string' ) { continue ; } else { const token = routeTokens [ i ] as any ; params [ token . name ] = patternResult [ patternResultIndex ++ ] ; } } result . push ( { params , route } ) ; return result ; } , [ ] as RouteMatch [ ] ) ; } deprecated getMatchingRoutes ( request ) { const url = ! request . url . startsWith ( 'http' ) ? new URL ( `http://domain${ request . url . startsWith ( '/' ) ? '' : '/' }${ request . url }` ) : new URL ( request . url ) ; return this . getMatchingRoutesForURLAndMethod ( url , request . method ) ; } param event returns async getResponseForEvent ( event ) { const url = ! event . request . url . startsWith ( 'http' ) ? new URL ( `http://domain${ event . request . url . startsWith ( '/' ) ? '' : '/' }${ event . request . url }` ) : new URL ( event . request . url ) ; const matchingRoutes = this . getMatchingRoutesForURLAndMethod ( url , event . request . method ) ; if ( matchingRoutes . length === 0 ) return ; const data = { } ; const response = new Response ( ) ; let ctx = new Context ( { event , response , params : { } , data , url , } ) ; let index = - 1 ; const dispatch = ( i ) : Promise < Response | undefined > | void => { if ( i <= index ) return Promise . reject ( new Error ( 'next() called multiple times' ) ) ; if ( i === matchingRoutes . length ) return ; index = i ; const { route , params } = matchingRoutes [ i ] ; ( ctx as any ) . params = params ; try { return Promise . resolve ( route . handler ( ctx , async ( ) => { await Promise . resolve ( dispatch ( i + 1 ) || null ) ; ( ctx as any ) . params = params ; } ) ) ; } catch ( err ) { return Promise . reject ( err ) ; } } ; const p = dispatch ( 0 ) ; if ( p ) return p . then ( ( ) => ctx . response ) ; return null ; } }