import express from 'express' ; import hsts from 'hsts' ; import cookiesMiddleware from 'universal-cookie-express' ; import ProjectServer from 'pawProjectServer' ; import { createHash } from 'crypto' ; import LRU from 'lru-cache' ; import request from 'supertest' ; import RouteHandler from '../router/handler' ; import ServerHandler from './handler' ; import env from '../config' ; import { getFullRequestUrl } from '../utils/server' ; import { RouteMatch } from 'react-router' ; type { * | Function } const app = express ( ) ; app . use ( cookiesMiddleware ( ) ) ; type { RouteHandler } const rHandler = new RouteHandler ( { env : { ... env } , isServer : true , } ) ; let ProjectRoutes = false ; if ( env . serverSideRender ) { ProjectRoutes = require ( 'pawProjectRoutes' ) ; if ( ProjectRoutes . default ) ProjectRoutes = ProjectRoutes . default ; rHandler . addPlugin ( new ProjectRoutes ( { addPlugin : rHandler . addPlugin } ) ) ; } type { * } const sHandler = new ServerHandler ( { env : { ... env } , expressApp : app , } ) ; const serverMiddlewareList = [ ] ; sHandler . addPlugin ( new ProjectServer ( { addPlugin : sHandler . addPlugin , addMiddleware : ( middleware ) => { serverMiddlewareList . push ( middleware ) ; } , } ) ) ; const cacheLog = ( ... args ) => { if ( process . env . PAW_VERBOSE === 'true' ) { console . log ( ... args ) ; } } ; const cacheOptions = sHandler . getCache ( ) ; const existingRequests = { } ; const isStartCmd = process . env . PAW_START_CMD === 'true' ; if ( cacheOptions && ! isStartCmd ) { const max32BitInt = Math . pow ( 2 , 30 ) - 1 ; const internalHttpApp = request ( app ) ; const optMax = cacheOptions . max || 52428800 ; let optMaxAge = cacheOptions . maxAge ; const optReCache = ! ! cacheOptions . reCache ; if ( typeof optMaxAge === 'undefined' || isNaN ( optMaxAge ) ) { optMaxAge = 300000 ; } const cache = new LRU ( { max : optMax , maxAge : optMaxAge , } ) ; /**
     * Add stale while revalidate caching
     */ app . use ( ( req , res , next ) => { if ( req . method === 'GET' ) { const requestHeaders = req . headers ; const fullUrl = getFullRequestUrl ( req ) ; const url = new URL ( fullUrl ) ; const byPassCache = url . searchParams . has ( '__no_cache' ) ; url . searchParams . delete ( '__no_cache' ) ; const nonOriginUrl = url . toString ( ) . replace ( url . origin , '' ) ; cacheLog ( `${ nonOriginUrl }:: byPass value: ${ byPassCache }` ) ; const defaultCacheKey = `__express__${ nonOriginUrl }` ; const cacheKey = createHash ( 'sha1' ) . update ( sHandler . getCacheKey ?. ( req , res ) || defaultCacheKey ) . digest ( 'base64' ) ; const reCacheRequest = ( ) => { if ( existingRequests [ cacheKey ] === true ) { return ; } existingRequests [ cacheKey ] = true ; url . searchParams . set ( '__no_cache' , 'true' ) ; const nonOriginUrlWithNoCache = url . toString ( ) . replace ( url . origin , '' ) ; url . searchParams . delete ( '__no_cache' ) ; cacheLog ( `${ nonOriginUrl }:: Re-Caching in the background` ) ; const internalReq = internalHttpApp . get ( nonOriginUrlWithNoCache ) ; if ( requestHeaders ) { internalReq . set ( requestHeaders ) ; } internalReq . then ( ( ) => { existingRequests [ cacheKey ] = false ; } ) ; } ; if ( ! byPassCache && cache . has ( cacheKey ) ) { cacheLog ( `${ nonOriginUrl }:: Cache found` ) ; const cachedData = cache . get ( cacheKey ) ; if ( cachedData && cachedData . headers && cachedData . statusCode && cachedData . data ) { const { [ 'content-encoding' ] : contentEncoding , vary , ... otherHeaders } = cachedData . headers ; cacheLog ( `${ nonOriginUrl }:: Sending cached` ) ; res . set ( otherHeaders ) ; res . status ( cachedData . statusCode ) ; res . write ( cachedData . data ) ; reCacheRequest ( ) ; return res . end ( ) ; } } if ( byPassCache ) { cacheLog ( `${ nonOriginUrl }:: Caching was not used/checked as __no_cache was found in the url` ) ; } let interceptedData = '' ; res . locals . cachedWrite = ( data ) => { interceptedData += data ; return res . write ( data ) ; } ; res . on ( 'finish' , ( ) => { if ( res . statusCode >= 200 && res . statusCode < 300 ) { try { const headers = res . getHeaders ( ) ; cacheLog ( `${ nonOriginUrl }:: Setting new cache` ) ; let maxAge = optMaxAge ; if ( res . locals . currentPageRoutes && Array . isArray ( res . locals . currentPageRoutes ) && res . locals . currentPageRoutes . length ) { res . locals . currentPageRoutes . forEach ( ( match ) => { const { route } = match as any ; if ( typeof route . cache !== 'undefined' ) { if ( route . cache === false || route . cache < 0 ) { maxAge = - 1 ; } else if ( ! isNaN ( route . cache . maxAge ) ) { maxAge = route . cache . maxAge ; } } } ) ; } if ( maxAge > max32BitInt ) { maxAge = max32BitInt ; } cache . set ( cacheKey , { url : nonOriginUrl , headers : JSON . parse ( JSON . stringify ( headers ) ) , statusCode : res . statusCode , data : interceptedData , } , maxAge ) ; if ( optReCache && maxAge > 0 ) { setTimeout ( ( ) => { if ( ! cache . has ( cacheKey ) ) { reCacheRequest ( ) ; } else { cacheLog ( `${ nonOriginUrl }:: Still have cache, thus not executing, re-cache` ) ; } } , maxAge + 1 ) ; } } catch ( ex ) { cacheLog ( ex ) ; } } } ) ; } return next ( ) ; } ) ; } else { app . use ( ( _req , res , next ) => { res . locals . cachedWrite = ( data ) => { return res . write ( data ) ; } ; next ( ) ; } ) ; } app . disable ( 'x-powered-by' ) ; /**
 * Enable trust proxy, i.e. allow data from
 * X-Protocol
 * X-Host
 * X-Forwarded-For
 */ app . enable ( 'trust proxy' ) ; type { { enabled : * , maxAge : * , includeSubDomains : * , preload : * } } const hstsSettings = { enabled : env . hstsEnabled , maxAge : env . hstsMaxAge , includeSubDomains : env . hstsIncludeSubDomains , preload : env . hstsPreload , } ; if ( hstsSettings . enabled ) { app . use ( ( req , res , next ) => { if ( req . secure ) { const hstsMiddleware = hsts ( hstsSettings ) ; hstsMiddleware ( req , res , next ) ; } else { next ( ) ; } } ) ; } serverMiddlewareList . forEach ( ( middleware ) => { app . use ( middleware ) ; } ) ; app . get ( `${ env . appRootUrl }/manifest.json` , ( req , res ) => { res . json ( rHandler . getPwaSchema ( ) ) ; } ) ; const assetExtensions = /\.(jpg|jpeg|gif|svg|mov|bmp|css|js|png|webp|pdf|doc|docx|json)/ ; const isAssetRequest = ( req ) => { const fullUrl = getFullRequestUrl ( req ) ; const parsedUrl = new URL ( fullUrl ) ; if ( ! parsedUrl . pathname ) return false ; return assetExtensions . test ( parsedUrl . pathname ) ; } ; app . get ( '*' , ( req , res , next ) => { if ( req . path . endsWith ( 'favicon.png' ) || req . path . endsWith ( 'favicon.ico' ) || req . path . endsWith ( 'favicon.jpg' ) || req . path . endsWith ( 'favicon.jpeg' ) || isAssetRequest ( req ) ) { return next ( ) ; } const fullUrl = `${ req . protocol }://${ req . get ( 'host' ) }${ req . originalUrl }` ; const clientRouteHandler = new RouteHandler ( { env : { ... env } , isServer : true , } ) ; if ( env . serverSideRender && ProjectRoutes ) { clientRouteHandler . addPlugin ( new ProjectRoutes ( { addPlugin : clientRouteHandler . addPlugin } ) ) ; } if ( ! env . serverSideRender ) { return sHandler . run ( { req , res , next , routeHandler : clientRouteHandler , } ) ; } return clientRouteHandler . hooks . initRoutes . callAsync ( fullUrl , req . headers ?. [ 'user-agent' ] ?? '' , ( err ) => { if ( err ) { console . log ( err ) ; return next ( ) ; } return sHandler . run ( { req , res , next , routeHandler : clientRouteHandler , } ) ; } ) ; } ) ; export { app } ; param req param res param next param PAW_GLOBAL returns { * } export default ( req , res , next , PAW_GLOBAL ) => { Object . keys ( PAW_GLOBAL ) . forEach ( ( key ) => { const val = PAW_GLOBAL [ key ] ; app . locals [ key ] = val ; serverMiddlewareList . forEach ( ( sm ) => { if ( sm && sm . locals ) { sm . locals [ key ] = val ; } } ) ; } ) ; return app . handle ( req , res , next ) ; } ; export const beforeStart = ( serverConfig , PAW_GLOBAL , cb = function callback ( ) { } ) => { const setAppLocal = ( key , value ) => { if ( ! key ) return ; PAW_GLOBAL [ key ] = value ; } ; const getAppLocal = ( key , defaultValue = false ) => { if ( ! PAW_GLOBAL [ key ] ) return defaultValue ; return PAW_GLOBAL [ key ] ; } ; sHandler . hooks . beforeStart . callAsync ( serverConfig , { setAppLocal , getAppLocal , } , cb ) ; } ; export const afterStart = ( PAW_GLOBAL , cb = function callback ( ) { } ) => { const setAppLocal = ( key , value ) => { if ( ! key ) return ; PAW_GLOBAL [ key ] = value ; } ; const getAppLocal = ( key , defaultValue = false ) => { if ( ! PAW_GLOBAL [ key ] ) return defaultValue ; return PAW_GLOBAL [ key ] ; } ; sHandler . hooks . afterStart . callAsync ( { setAppLocal , getAppLocal , } , cb ) ; } ;