@ -5,17 +5,9 @@ const url = require('url')
const debug = require ( 'debug' ) ( 'local-web-server' )
const mw = require ( './middleware' )
const t = require ( 'typical' )
const compose = require ( 'koa-compose' )
class MiddlewareStack extends Array {
constructor ( options ) {
super ( )
this . options = options
if ( options . verbose ) {
process . env . DEBUG = '*'
}
}
add ( middleware ) {
this . push ( middleware )
return this
@ -25,141 +17,196 @@ class MiddlewareStack extends Array {
* allow from any origin
* /
addCors ( ) {
this . push ( require ( 'kcors' ) ( ) )
this . push ( { middleware : require ( 'kcors' ) } )
return this
}
/* pretty print JSON */
addJson ( ) {
this . push ( require ( 'koa-json' ) ( ) )
this . push ( { middleware : require ( 'koa-json' ) } )
return this
}
/* rewrite rules */
addRewrite ( rewriteRules ) {
const options = arrayify ( this . options . server . rewrite || rewriteRules )
if ( options . length ) {
options . forEach ( route => {
if ( route . to ) {
/* `to` address is remote if the url specifies a host */
if ( url . parse ( route . to ) . host ) {
const _ = require ( 'koa-route' )
debug ( 'proxy rewrite' , ` ${ route . from } -> ${ route . to } ` )
this . push ( _ . all ( route . from , mw . proxyRequest ( route ) ) )
} else {
const rewrite = require ( 'koa-rewrite' )
const rmw = rewrite ( route . from , route . to )
rmw . _name = 'rewrite'
this . push ( rmw )
}
this . push ( {
optionDefinitions : {
name : 'rewrite' , alias : 'r' , type : String , multiple : true ,
typeLabel : '[underline]{expression} ...' ,
description : "A list of URL rewrite rules. For each rule, separate the 'from' and 'to' routes with '->'. Whitespace surrounded the routes is ignored. E.g. '/from -> /to'."
} ,
middleware : function ( cliOptions ) {
const options = arrayify ( cliOptions . middleware . rewrite || rewriteRules )
if ( options . length ) {
options . forEach ( route => {
if ( route . to ) {
/* `to` address is remote if the url specifies a host */
if ( url . parse ( route . to ) . host ) {
const _ = require ( 'koa-route' )
debug ( 'proxy rewrite' , ` ${ route . from } -> ${ route . to } ` )
return _ . all ( route . from , mw . proxyRequest ( route ) )
} else {
const rewrite = require ( 'koa-rewrite' )
const rmw = rewrite ( route . from , route . to )
rmw . _name = 'rewrite'
return rmw
}
}
} )
}
} )
}
}
} )
return this
}
/ * m u s t c o m e a f t e r r e w r i t e .
See https : //github.com/nodejitsu/node-http-proxy/issues/180. */
addBodyParser ( ) {
this . push ( require ( 'koa-bodyparser' ) ( ) )
this . push ( { middleware : require ( 'koa-bodyparser' ) } )
return this
}
/* path blacklist */
addBlacklist ( forbidList ) {
forbidList = arrayify ( this . options . server . forbid || forbidList )
if ( forbidList . length ) {
const pathToRegexp = require ( 'path-to-regexp' )
debug ( 'forbid' , forbidList . join ( ', ' ) )
this . push ( function blacklist ( ctx , next ) {
if ( forbidList . some ( expression => pathToRegexp ( expression ) . test ( ctx . path ) ) ) {
ctx . throw ( 403 , http . STATUS_CODES [ 403 ] )
} else {
return next ( )
this . push ( {
optionDefinitions : {
name : 'forbid' , alias : 'b' , type : String ,
multiple : true , typeLabel : '[underline]{path} ...' ,
description : 'A list of forbidden routes.'
} ,
middleware : function ( cliOptions ) {
forbidList = arrayify ( cliOptions . middleware . forbid || forbidList )
if ( forbidList . length ) {
const pathToRegexp = require ( 'path-to-regexp' )
debug ( 'forbid' , forbidList . join ( ', ' ) )
return function blacklist ( ctx , next ) {
if ( forbidList . some ( expression => pathToRegexp ( expression ) . test ( ctx . path ) ) ) {
const http = require ( 'http' )
ctx . throw ( 403 , http . STATUS_CODES [ 403 ] )
} else {
return next ( )
}
}
}
} )
}
}
} )
return this
}
/* cache */
addCache ( ) {
const noCache = this . options . server [ 'no-cache' ]
if ( ! noCache ) {
this . push ( require ( 'koa-conditional-get' ) ( ) )
this . push ( require ( 'koa-etag' ) ( ) )
}
this . push ( {
optionDefinitions : {
name : 'no-cache' , alias : 'n' , type : Boolean ,
description : 'Disable etag-based caching - forces loading from disk each request.'
} ,
middleware : function ( cliOptions ) {
const noCache = cliOptions . middleware [ 'no-cache' ]
if ( ! noCache ) {
return [
require ( 'koa-conditional-get' ) ( ) ,
require ( 'koa-etag' ) ( )
]
}
}
} )
return this
}
/* mime-type overrides */
addMimeType ( mime ) {
mime = this . options . server . mime || mime
if ( mime ) {
debug ( 'mime override' , JSON . stringify ( mime ) )
this . push ( mw . mime ( mime ) )
}
this . push ( {
middleware : function ( cliOptions ) {
mime = cliOptions . middleware . mime || mime
if ( mime ) {
debug ( 'mime override' , JSON . stringify ( mime ) )
return mw . mime ( mime )
}
}
} )
return this
}
/* compress response */
addCompression ( compress ) {
compress = t . isDefined ( this . options . server . compress )
? this . options . server . compress
: compress
if ( compress ) {
debug ( 'compression' , 'enabled' )
this . push ( require ( 'koa-compress' ) ( ) )
}
this . push ( {
optionDefinitions : {
name : 'compress' , alias : 'c' , type : Boolean ,
description : 'Serve gzip-compressed resources, where applicable.'
} ,
middleware : function ( cliOptions ) {
compress = t . isDefined ( cliOptions . middleware . compress )
? cliOptions . middleware . compress
: compress
if ( compress ) {
debug ( 'compression' , 'enabled' )
return require ( 'koa-compress' ) ( )
}
}
} )
return this
}
/* Logging */
addLogging ( format , options ) {
format = this . options . server [ 'log-format' ] || format
options = options || { }
this . push ( {
optionDefinitions : {
name : 'log-format' ,
alias : 'f' ,
type : String ,
description : "If a format is supplied an access log is written to stdout. If not, a dynamic statistics view is displayed. Use a preset ('none', 'dev','combined', 'short', 'tiny' or 'logstalgia') or supply a custom format (e.g. ':method -> :url')."
} ,
middleware : function ( cliOptions ) {
format = cliOptions . middleware [ 'log-format' ] || format
if ( this . options . verbose && ! format ) {
format = 'none'
}
if ( format !== 'none' ) {
const morgan = require ( 'koa-morgan' )
if ( ! format ) {
const streamLogStats = require ( 'stream-log-stats' )
options . stream = streamLogStats ( { refreshRate : 500 } )
this . push ( morgan ( 'common' , options ) )
} else if ( format === 'logstalgia' ) {
morgan . token ( 'date' , ( ) => {
var d = new Date ( )
return ( ` ${ d . getDate ( ) } / ${ d . getUTCMonth ( ) } / ${ d . getFullYear ( ) } : ${ d . toTimeString ( ) } ` ) . replace ( 'GMT' , '' ) . replace ( ' (BST)' , '' )
} )
this . push ( morgan ( 'combined' , options ) )
} else {
this . push ( morgan ( format , options ) )
if ( cliOptions . misc . verbose && ! format ) {
format = 'none'
}
if ( format !== 'none' ) {
const morgan = require ( 'koa-morgan' )
if ( ! format ) {
const streamLogStats = require ( 'stream-log-stats' )
options . stream = streamLogStats ( { refreshRate : 500 } )
return morgan ( 'common' , options )
} else if ( format === 'logstalgia' ) {
morgan . token ( 'date' , ( ) => {
var d = new Date ( )
return ( ` ${ d . getDate ( ) } / ${ d . getUTCMonth ( ) } / ${ d . getFullYear ( ) } : ${ d . toTimeString ( ) } ` ) . replace ( 'GMT' , '' ) . replace ( ' (BST)' , '' )
} )
return morgan ( 'combined' , options )
} else {
return morgan ( format , options )
}
}
}
}
} )
return this
}
/* Mock Responses */
addMockResponses ( mocks ) {
mocks = arrayify ( this . options . server . mocks || mocks )
mocks . forEach ( mock => {
if ( mock . module ) {
// TODO: ENSURE this.options.static.root is correct value
mock . responses = require ( path . resolve ( path . join ( this . options . static . root , mock . module ) ) )
}
this . push ( {
middleware : function ( cliOptions ) {
mocks = arrayify ( cliOptions . middleware . mocks || mocks )
mocks . forEach ( mock => {
if ( mock . module ) {
// TODO: ENSURE cliOptions.static.root is correct value
mock . responses = require ( path . resolve ( path . join ( cliOptions . static . root , mock . module ) ) )
}
if ( mock . responses ) {
this . push ( mw . mockResponses ( mock . route , mock . responses ) )
} else if ( mock . response ) {
mock . target = {
request : mock . request ,
response : mock . response
}
this . push ( mw . mockResponses ( mock . route , mock . target ) )
if ( mock . responses ) {
return mw . mockResponses ( mock . route , mock . responses )
} else if ( mock . response ) {
mock . target = {
request : mock . request ,
response : mock . response
}
return mw . mockResponses ( mock . route , mock . target )
}
} )
}
} )
return this
@ -167,44 +214,77 @@ class MiddlewareStack extends Array {
/* for any URL not matched by static (e.g. `/search`), serve the SPA */
addSpa ( spa ) {
spa = t . isDefined ( this . options . server . spa ) ? this . options . server . spa : spa
if ( spa ) {
const historyApiFallback = require ( 'koa-connect-history-api-fallback' )
debug ( 'SPA' , spa )
this . push ( historyApiFallback ( {
index : spa ,
verbose : this . options . verbose
} ) )
}
this . push ( {
optionDefinitions : {
name : 'spa' , alias : 's' , type : String , typeLabel : '[underline]{file}' ,
description : 'Path to a Single Page App, e.g. app.html.'
} ,
middleware : function ( cliOptions ) {
spa = t . isDefined ( cliOptions . middleware . spa ) ? cliOptions . middleware . spa : spa
if ( spa ) {
const historyApiFallback = require ( 'koa-connect-history-api-fallback' )
debug ( 'SPA' , spa )
return historyApiFallback ( {
index : spa ,
verbose : cliOptions . misc . verbose
} )
}
}
} )
return this
}
/* serve static files */
addStatic ( root , options ) {
root = this . options . server . directory || root || process . cwd ( )
options = Object . assign ( { hidden : true } , options )
if ( root ) {
const serve = require ( 'koa-static' )
this . push ( serve ( root , options ) )
}
this . push ( {
optionDefinitions : {
name : 'directory' , alias : 'd' , type : String , typeLabel : '[underline]{path}' ,
description : 'Root directory, defaults to the current directory.'
} ,
middleware : function ( cliOptions ) {
root = cliOptions . middleware . directory || root || process . cwd ( )
options = Object . assign ( { hidden : true } , options )
// console.log(root, options, cliOptions)
if ( root ) {
const serve = require ( 'koa-static' )
return serve ( root , options )
}
}
} )
return this
}
/* serve directory index */
addIndex ( path , options ) {
path = this . options . server . directory || path || process . cwd ( )
options = Object . assign ( { icons : true , hidden : true } , options )
if ( path ) {
const serveIndex = require ( 'koa-serve-index' )
this . push ( serveIndex ( path , options ) )
}
this . push ( {
middleware : function ( cliOptions ) {
path = cliOptions . middleware . directory || path || process . cwd ( )
options = Object . assign ( { icons : true , hidden : true } , options )
if ( path ) {
const serveIndex = require ( 'koa-serve-index' )
return serveIndex ( path , options )
}
}
} )
return this
}
getOptionDefinitions ( ) {
const flatten = require ( 'reduce-flatten' )
return this
. filter ( mw => mw . optionDefinitions )
. map ( mw => mw . optionDefinitions )
. reduce ( flatten , [ ] )
. map ( def => {
def . group = 'middleware'
return def
} )
}
compose ( options ) {
const compose = require ( 'koa-compose' )
const convert = require ( 'koa-convert' )
const middlewareStack = this . map ( convert )
const middlewareStack = this
. filter ( mw => mw )
. map ( mw => compose ( arrayify ( mw . middleware ( options ) ) . map ( convert ) ) )
return compose ( middlewareStack )
}
}