2016-06-08 23:06:41 +01:00
'use strict'
const arrayify = require ( 'array-back' )
const path = require ( 'path' )
const url = require ( 'url' )
2016-06-18 10:13:27 +01:00
const debug = require ( './debug' )
2016-06-08 23:06:41 +01:00
const mw = require ( './middleware' )
2016-06-15 21:02:52 +01:00
const t = require ( 'typical' )
2016-06-16 23:00:07 +01:00
const compose = require ( 'koa-compose' )
2016-06-18 10:13:27 +01:00
const flatten = require ( 'reduce-flatten' )
2016-06-08 23:06:41 +01:00
class MiddlewareStack extends Array {
2016-06-09 22:54:57 +01:00
add ( middleware ) {
this . push ( middleware )
return this
2016-06-08 23:06:41 +01:00
}
/ * *
* allow from any origin
* /
addCors ( ) {
2016-06-16 23:00:07 +01:00
this . push ( { middleware : require ( 'kcors' ) } )
2016-06-08 23:06:41 +01:00
return this
}
/* pretty print JSON */
addJson ( ) {
2016-06-16 23:00:07 +01:00
this . push ( { middleware : require ( 'koa-json' ) } )
2016-06-08 23:06:41 +01:00
return this
}
/* rewrite rules */
2016-06-15 21:02:52 +01:00
addRewrite ( rewriteRules ) {
2016-06-16 23:00:07 +01:00
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 ) {
2016-06-18 10:37:52 +01:00
const options = parseRewriteRules ( arrayify ( cliOptions . rewrite || rewriteRules ) )
2016-06-16 23:00:07 +01:00
if ( options . length ) {
2016-06-18 10:13:27 +01:00
return options . map ( route => {
2016-06-16 23:00:07 +01:00
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
}
}
} )
2016-06-08 23:06:41 +01:00
}
2016-06-16 23:00:07 +01:00
}
} )
2016-06-08 23:06:41 +01:00
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 ( ) {
2016-06-16 23:00:07 +01:00
this . push ( { middleware : require ( 'koa-bodyparser' ) } )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* path blacklist */
2016-06-15 21:02:52 +01:00
addBlacklist ( forbidList ) {
2016-06-16 23:00:07 +01:00
this . push ( {
optionDefinitions : {
name : 'forbid' , alias : 'b' , type : String ,
multiple : true , typeLabel : '[underline]{path} ...' ,
description : 'A list of forbidden routes.'
} ,
middleware : function ( cliOptions ) {
2016-06-18 10:13:27 +01:00
forbidList = arrayify ( cliOptions . forbid || forbidList )
2016-06-16 23:00:07 +01:00
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 ) ) ) {
2016-06-20 01:02:51 +01:00
ctx . status = 403
2016-06-16 23:00:07 +01:00
} else {
return next ( )
}
}
2016-06-15 21:02:52 +01:00
}
2016-06-16 23:00:07 +01:00
}
} )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* cache */
addCache ( ) {
2016-06-16 23:00:07 +01:00
this . push ( {
optionDefinitions : {
name : 'no-cache' , alias : 'n' , type : Boolean ,
description : 'Disable etag-based caching - forces loading from disk each request.'
} ,
middleware : function ( cliOptions ) {
2016-06-18 10:13:27 +01:00
const noCache = cliOptions [ 'no-cache' ]
2016-06-16 23:00:07 +01:00
if ( ! noCache ) {
return [
require ( 'koa-conditional-get' ) ( ) ,
require ( 'koa-etag' ) ( )
]
}
}
} )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* mime-type overrides */
2016-06-20 01:02:51 +01:00
addMimeOverride ( mime ) {
2016-06-16 23:00:07 +01:00
this . push ( {
middleware : function ( cliOptions ) {
2016-06-18 10:13:27 +01:00
mime = cliOptions . mime || mime
2016-06-16 23:00:07 +01:00
if ( mime ) {
debug ( 'mime override' , JSON . stringify ( mime ) )
return mw . mime ( mime )
}
}
} )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* compress response */
2016-06-15 21:02:52 +01:00
addCompression ( compress ) {
2016-06-16 23:00:07 +01:00
this . push ( {
optionDefinitions : {
name : 'compress' , alias : 'c' , type : Boolean ,
description : 'Serve gzip-compressed resources, where applicable.'
} ,
middleware : function ( cliOptions ) {
2016-06-18 10:13:27 +01:00
compress = t . isDefined ( cliOptions . compress )
? cliOptions . compress
2016-06-16 23:00:07 +01:00
: compress
if ( compress ) {
debug ( 'compression' , 'enabled' )
return require ( 'koa-compress' ) ( )
}
}
} )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* Logging */
2016-06-09 22:54:57 +01:00
addLogging ( format , options ) {
options = options || { }
2016-06-16 23:00:07 +01:00
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 ) {
2016-06-18 10:13:27 +01:00
format = cliOptions [ 'log-format' ] || format
2016-06-09 22:54:57 +01:00
2016-06-18 10:13:27 +01:00
if ( cliOptions . verbose && ! format ) {
2016-06-16 23:00:07 +01:00
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 )
}
}
2016-06-08 23:06:41 +01:00
}
2016-06-16 23:00:07 +01:00
} )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* Mock Responses */
2016-06-15 21:02:52 +01:00
addMockResponses ( mocks ) {
2016-06-16 23:00:07 +01:00
this . push ( {
middleware : function ( cliOptions ) {
2016-06-18 10:13:27 +01:00
mocks = arrayify ( cliOptions . mocks || mocks )
return mocks . map ( mock => {
2016-06-16 23:00:07 +01:00
if ( mock . module ) {
2016-06-18 10:13:27 +01:00
const modulePath = path . resolve ( path . join ( cliOptions . directory , mock . module ) )
mock . responses = require ( modulePath )
2016-06-16 23:00:07 +01:00
}
2016-06-08 23:06:41 +01:00
2016-06-16 23:00:07 +01:00
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 )
}
} )
2016-06-08 23:06:41 +01:00
}
} )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* for any URL not matched by static (e.g. `/search`), serve the SPA */
2016-06-18 11:39:25 +01:00
addSpa ( spa , assetTest ) {
2016-06-16 23:00:07 +01:00
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 ) {
2016-06-18 11:39:25 +01:00
spa = cliOptions . spa || spa || 'index.html'
assetTest = new RegExp ( cliOptions [ 'spa-asset-test' ] || assetTest || '\\.' )
2016-06-16 23:00:07 +01:00
if ( spa ) {
2016-06-18 11:39:25 +01:00
const send = require ( 'koa-send' )
const _ = require ( 'koa-route' )
2016-06-16 23:00:07 +01:00
debug ( 'SPA' , spa )
2016-06-18 11:39:25 +01:00
return _ . get ( '*' , function spaMw ( ctx , route , next ) {
const root = path . resolve ( cliOptions . directory || process . cwd ( ) )
2016-06-20 01:02:51 +01:00
if ( ctx . accepts ( 'text/html' ) && ! assetTest . test ( route ) ) {
debug ( ` SPA request. Route: ${ route } , isAsset: ${ assetTest . test ( route ) } ` )
return send ( ctx , spa , { root : root } ) . then ( next )
} else {
return send ( ctx , route , { root : root } ) . then ( next )
}
2016-06-16 23:00:07 +01:00
} )
}
}
} )
2016-06-09 22:54:57 +01:00
return this
2016-06-08 23:06:41 +01:00
}
/* serve static files */
2016-06-09 22:54:57 +01:00
addStatic ( root , options ) {
2016-06-16 23:00:07 +01:00
this . push ( {
optionDefinitions : {
name : 'directory' , alias : 'd' , type : String , typeLabel : '[underline]{path}' ,
description : 'Root directory, defaults to the current directory.'
} ,
middleware : function ( cliOptions ) {
2016-06-18 21:17:20 +01:00
/* update global cliOptions */
2016-06-18 10:13:27 +01:00
cliOptions . directory = cliOptions . directory || root || process . cwd ( )
2016-06-16 23:00:07 +01:00
options = Object . assign ( { hidden : true } , options )
2016-06-18 10:13:27 +01:00
if ( cliOptions . directory ) {
2016-06-16 23:00:07 +01:00
const serve = require ( 'koa-static' )
2016-06-18 10:13:27 +01:00
return serve ( cliOptions . directory , options )
2016-06-16 23:00:07 +01:00
}
}
} )
2016-06-08 23:06:41 +01:00
return this
}
/* serve directory index */
2016-06-09 22:54:57 +01:00
addIndex ( path , options ) {
2016-06-16 23:00:07 +01:00
this . push ( {
middleware : function ( cliOptions ) {
2016-06-18 10:13:27 +01:00
path = cliOptions . directory || path || process . cwd ( )
2016-06-16 23:00:07 +01:00
options = Object . assign ( { icons : true , hidden : true } , options )
if ( path ) {
const serveIndex = require ( 'koa-serve-index' )
return serveIndex ( path , options )
}
}
} )
2016-06-08 23:06:41 +01:00
return this
}
2016-06-16 23:00:07 +01:00
getOptionDefinitions ( ) {
return this
. filter ( mw => mw . optionDefinitions )
. map ( mw => mw . optionDefinitions )
. reduce ( flatten , [ ] )
. map ( def => {
def . group = 'middleware'
return def
} )
}
2016-06-15 21:02:52 +01:00
compose ( options ) {
2016-06-08 23:06:41 +01:00
const convert = require ( 'koa-convert' )
2016-06-16 23:00:07 +01:00
const middlewareStack = this
2016-06-18 10:13:27 +01:00
. filter ( mw => mw . middleware )
. map ( mw => mw . middleware )
. map ( middleware => middleware ( options ) )
. filter ( middleware => middleware )
. reduce ( flatten , [ ] )
. map ( convert )
// console.error(require('util').inspect(middlewareStack, { depth: 3, colors: true }))
2016-06-08 23:06:41 +01:00
return compose ( middlewareStack )
}
}
module . exports = MiddlewareStack
2016-06-18 10:37:52 +01:00
function parseRewriteRules ( rules ) {
return rules && rules . map ( rule => {
if ( t . isString ( rule ) ) {
const matches = rule . match ( /(\S*)\s*->\s*(\S*)/ )
if ( ! ( matches && matches . length >= 3 ) ) throw new Error ( 'Invalid rule: ' + rule )
return {
from : matches [ 1 ] ,
to : matches [ 2 ]
}
} else {
return rule
}
} )
}