import { StringDecoder } from "string_decoder" ; import { readFileSync , existsSync } from "fs" ; import { join , extname } from "path" ; import * as http from "http" ; import * as url from "url" ; export interface IRequestContext { path : string ; method : string ; params : { [ key ] : string ; } ; query : { [ key ] : string | string [ ] | undefined ; } ; headers : http . IncomingHttpHeaders ; payload ? : any ; auth ? : any ; attachment ? : string ; status ? : number ; body ? : any ; buffer ? : string ; type ? : string ; } export type RequestHandler = ( ctx : IRequestContext , next ? : ( ) => Promise < void > ) => Promise < void > ; export interface IParam { name : string ; index : number ; } export interface IRoute { method : string ; path : string ; handlers : RequestHandler [ ] ; params : IParam [ ] ; } export class Router { public prefix : string ; public routes : IRoute [ ] = [ ] ; public extantions : Router [ ] = [ ] ; constructor ( prefix = "" ) { this . prefix = prefix . replace ( /^\/+|\/+$/g , "" ) ; } public attach ( router ) { this . extantions . push ( router ) ; } public inject ( httpServer ) { const listeners = httpServer . listeners ( "request" ) . slice ( 0 ) ; httpServer . removeAllListeners ( "request" ) ; httpServer . on ( "request" , ( req , res ) => { const route = this . has ( req . method || "" , req . url || "" ) ; if ( route ) { this . exec ( route , req , res ) ; } else { for ( const listener of listeners ) { listener . call ( httpServer , req , res ) ; } } } ) ; } public callback ( ) { return ( req , res ) => { const route = this . has ( req . method || "" , req . url || "" ) ; if ( route ) { this . exec ( route , req , res ) ; } else { res . statusCode = 404 ; } } ; } public has ( reqMethod , reqUrl ) { const parsedURL = url . parse ( reqUrl , true ) ; if ( ! parsedURL . pathname ) { return null ; } reqUrl = parsedURL . pathname . replace ( /^\/+|\/+$/g , "" ) ; if ( reqUrl . substring ( 0 , this . prefix . length ) !== this . prefix ) { return null ; } reqMethod = reqMethod && reqMethod . toLocaleLowerCase ( ) || "" ; let route = this . routes . find ( ( { path , method } ) => { const pathArr = ( this . prefix + path ) . replace ( /^\/+|\/+$/g , "" ) . split ( "/" ) . map ( ( item ) => item [ 0 ] === ":" ? "[A-Za-z0-9_.\-]+" : item ) ; const pathEnd = ( path . slice ( - 1 ) === "?" ) ? `(\/${ pathArr . pop ( ) })?$` : "$" ; const regExpPath = new RegExp ( pathArr . join ( "\/" ) + pathEnd ) ; return method === reqMethod && regExpPath . test ( reqUrl ) ; } ) || null ; if ( ! route ) { for ( const router of this . extantions ) { route = router . has ( reqMethod , reqUrl . slice ( this . prefix . length ) ) ; if ( route ) { break ; } } } return route ; } public exec ( route , req , res ) { const parsedURL = url . parse ( req . url || "" , true ) ; const path = ( parsedURL . pathname || "" ) . replace ( /^\/+|\/+$/g , "" ) ; let buffer = "" ; const pathArr = path . split ( "/" ) ; const params = { } as any ; route . params . forEach ( ( param ) => { params [ param . name ] = pathArr [ param . index ] ; } ) ; const decoder = new StringDecoder ( "utf-8" ) ; let index = 0 ; const ctx = { path , query : parsedURL . query , params , headers : req . headers , method : ( req . method || "" ) . toLowerCase ( ) , } ; const next = async ( ) => { await route . handlers [ index ++ ] ( ctx , next ) ; } ; req . on ( "data" , ( chunk ) => buffer += decoder . write ( chunk ) ) ; req . on ( "end" , async ( ) => { buffer += decoder . end ( ) ; if ( req . headers [ "content-type" ] ?. includes ( "application/json" ) ) { ctx . payload = JSON . parse ( buffer ) ; } else { ctx . payload = buffer ; } try { await next ( ) ; } catch ( error ) { ctx . status = 400 ; ctx . body = error ; } const headers = { "Access-Control-Allow-Headers" : "Origin, X-Requested-With, Content-Type, Accept" , "Access-Control-Allow-Methods" : "OPTIONS, POST, GET, DELETE" , "Access-Control-Allow-Origin" : "*" , "Access-Control-Max-Age" : 2592000 , } ; if ( ctx . attachment ) { headers [ "Content-Disposition" ] = `attachment; filename=${ ctx . attachment }` ; } if ( ctx . buffer ) { headers [ "Content-Type" ] = `text/${ ctx . type }; charset=utf-8` ; } else if ( ctx . body ) { headers [ "Content-Type" ] = "application/json" ; } res . writeHead ( ctx . status || 200 , headers ) ; res . end ( ctx . buffer || JSON . stringify ( ctx . body ) || "" ) ; } ) ; } public addRoute ( method , path , handlers ) { const pathArr = ( this . prefix + path ) . replace ( /^\/+|\/+$|\?+$/g , "" ) . split ( "/" ) ; const params = [ ] ; pathArr . forEach ( ( item , index ) => item [ 0 ] === ":" && params . push ( { name : item . slice ( 1 ) , index } ) ) ; this . routes . push ( { method , path , handlers , params } ) ; return this ; } public get ( path , ... handlers ) { return this . addRoute ( "get" , path , handlers ) ; } public post ( path , ... handlers ) { return this . addRoute ( "post" , path , handlers ) ; } public delete ( path , ... handlers ) { return this . addRoute ( "delete" , path , handlers ) ; } public static send ( ... paths ) { const path = join ( ... paths ) ; return async ( ctx ) : Promise < void > => { try { ctx . type = extname ( path ) . slice ( 1 ) ; ctx . buffer = await readFileSync ( path , { encoding : "utf-8" } ) ; } catch ( error ) { ctx . status = 404 ; } } ; } public static static ( path ) { const router = new Router ( ) ; router . has = ( reqMethod , reqUrl ) : IRoute | null => { reqUrl = reqUrl || "/" ; let fullpath = join ( path , reqUrl ) ; if ( reqUrl . slice ( - 1 ) === "/" ) { fullpath += "index.html" ; } if ( reqMethod !== "get" || ! existsSync ( fullpath ) ) { return null ; } const route = { method : "get" , path : fullpath , handlers : [ Router . send ( fullpath ) ] , params : [ ] , } ; return route ; } ; return router ; } }