Files
hiring-test-one/lib/middleware-stack.js

319 lines
9.3 KiB
JavaScript
Raw Normal View History

2016-06-08 23:06:41 +01:00
'use strict'
const arrayify = require('array-back')
const path = require('path')
const url = require('url')
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')
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) {
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
}
/* must come after rewrite.
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) {
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) {
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) {
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) {
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) {
format = cliOptions['log-format'] || format
2016-06-09 22:54:57 +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) {
mocks = arrayify(cliOptions.mocks || mocks)
return mocks.map(mock => {
2016-06-16 23:00:07 +01:00
if (mock.module) {
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 */
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) {
spa = cliOptions.spa || spa || 'index.html'
assetTest = new RegExp(cliOptions['spa-asset-test'] || assetTest || '\\.')
2016-06-16 23:00:07 +01:00
if (spa) {
const send = require('koa-send')
const _ = require('koa-route')
2016-06-16 23:00:07 +01:00
debug('SPA', spa)
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 */
cliOptions.directory = cliOptions.directory || root || process.cwd()
2016-06-16 23:00:07 +01:00
options = Object.assign({ hidden: true }, options)
if (cliOptions.directory) {
2016-06-16 23:00:07 +01:00
const serve = require('koa-static')
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) {
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
.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
}
})
}