import * as phantom from "phantom" ; import * as httpProxy from "http-proxy" ; import * as http from "http" ; import * as url from "url" ; import { Url } from "url" ; import { chain , debounce , keyBy , mapValues , filter , omit , isEmpty , partial } from "lodash" ; export default class SSRProxy { private server : http . Server ; private httpProxy : httpProxy . Proxy ; constructor ( private upstream , private idleMax = 500 , private nodeHttpProxyParams ? , private phantomjsParams ? ) { this . httpProxy = httpProxy . createProxyServer ( nodeHttpProxyParams ) ; this . server = http . createServer ( partial ( this . requestHandler , this ) ) ; } async requestHandler ( context , req , res ) { const instance = await phantom . create ( this . phantomjsParams ) ; console . info ( "[SSR-Proxy] Request" , req . url ) ; const debouncedWrite = debounce ( ( ) => { page . property ( "content" ) . then ( content => res . end ( content ) ) . then ( ( ) => instance . exit ( ) ) ; } , context . idleMax ) ; const srvUrl = url . parse ( `http://${ req . url }` ) ; if ( SSRProxy . shouldDirectProxy ( srvUrl ) ) { console . info ( "[SSR-Proxy] Direct proxy" , srvUrl . path ) ; return await context . httpProxy . web ( req , res , { target : context . upstream } ) ; } const page = await instance . createPage ( ) ; console . info ( "[Phantom] Page Created" ) ; await page . on ( "onResourceRequested" , true , function ( requestData , networkRequest ) { if ( SSRProxy . shouldPhantomjsIgnore ( networkRequest . url ) ) { return networkRequest . abort ( ) ; } console . info ( "[Phantom] Resource Requesting" , requestData . id , requestData . url ) ; debouncedWrite ( ) ; } ) ; await page . on ( "onResourceReceived" , ( requestData ) => { console . info ( "[Phantom] Resource Received" , requestData . url ) ; if ( requestData . id === 1 ) { let newheader = SSRProxy . rewriteHeader ( requestData . headers ) ; res . writeHead ( requestData . status , newheader ) ; } debouncedWrite ( ) ; } ) ; await page . on ( "onResourceError" , true , function ( requestData , networkRequest ) { if ( ! SSRProxy . shouldPhantomjsIgnore ( networkRequest . url ) ) { console . error ( "[Phantom] Resource Error" , requestData . url , requestData . errorString ) ; debouncedWrite ( ) ; } } ) ; try { const targetUrl = url . resolve ( context . upstream , srvUrl . path ) ; console . info ( "[Phantom] will open page" , targetUrl ) ; const status = await page . open ( targetUrl ) ; console . info ( "[Phantom] page open" , srvUrl . path , status ) ; } catch ( e ) { console . error ( "[Phantom] Failed to open targetUrl" , e ) ; } } ; listen ( port ) { return this . server . listen ( port ) ; } static rewriteHeader ( phantomRequestDataHeaders ) { return chain ( phantomRequestDataHeaders ) . keyBy ( "name" ) . mapValues ( "value" ) . omit ( [ "Content-Encoding" ] ) . value ( ) ; } ; static shouldDirectProxy ( srvUrl ) { return ! isEmpty ( srvUrl . pathname . match ( /(\.png$)|(\.jpg$)|(\.ttf$)/ ) ) ; } static shouldPhantomjsIgnore ( url ) { return ! isEmpty ( url . match ( /\.ttf$/ ) ) ; } }