You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

309 lines
9.0 KiB

'use strict'
const arrayify = require('array-back')
const path = require('path')
const url = require('url')
const debug = require('./debug')
const mw = require('./middleware')
const t = require('typical')
const compose = require('koa-compose')
const flatten = require('reduce-flatten')
const MiddlewareStack = require('local-web-server-stack')
const mockResponses = require('koa-mock-response')
class DefaultStack extends MiddlewareStack {
addAll () {
this
.addCors()
.addJson()
.addRewrite()
.addBodyParser()
.addBlacklist()
.addCache()
.addMimeOverride()
.addCompression()
.addLogging()
.addMockResponses()
.addSpa()
.addStatic()
.addIndex()
return this
}
/**
* allow from any origin
*/
addCors () {
this.push({ middleware: require('kcors') })
return this
}
/* pretty print JSON */
addJson () {
this.push({ middleware: require('koa-json') })
return this
}
/* rewrite rules */
addRewrite (rewriteRules) {
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 = parseRewriteRules(arrayify(cliOptions.rewrite || rewriteRules))
if (options.length) {
return options.map(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
}
/* must come after rewrite.
See https://github.com/nodejitsu/node-http-proxy/issues/180. */
addBodyParser () {
this.push({ middleware: require('koa-bodyparser') })
return this
}
/* path blacklist */
addBlacklist (forbidList) {
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)
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))) {
ctx.status = 403
} else {
return next()
}
}
}
}
})
return this
}
/* cache */
addCache () {
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']
if (!noCache) {
return [
require('koa-conditional-get')(),
require('koa-etag')()
]
}
}
})
return this
}
/* mime-type overrides */
addMimeOverride (mime) {
this.push({
middleware: function (cliOptions) {
mime = cliOptions.mime || mime
if (mime) {
debug('mime override', JSON.stringify(mime))
return mw.mime(mime)
}
}
})
return this
}
/* compress response */
addCompression (compress) {
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
: compress
if (compress) {
debug('compression', 'enabled')
return require('koa-compress')()
}
}
})
return this
}
/* Logging */
addLogging (format, options) {
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['log-format'] || format
if (cliOptions.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) {
this.push({
middleware: function (cliOptions) {
mocks = arrayify(cliOptions.mocks || mocks)
return mocks.map(mock => {
if (mock.module) {
const modulePath = path.resolve(path.join(cliOptions.directory, mock.module))
mock.responses = require(modulePath)
}
if (mock.responses) {
return mockResponses(mock.route, mock.responses)
} else if (mock.response) {
mock.target = {
request: mock.request,
response: mock.response
}
return mockResponses(mock.route, mock.target)
}
})
}
})
return this
}
/* for any URL not matched by static (e.g. `/search`), serve the SPA */
addSpa (spa, assetTest) {
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 || '\\.')
if (spa) {
const send = require('koa-send')
const _ = require('koa-route')
debug('SPA', spa)
return _.get('*', function spaMw (ctx, route, next) {
const root = path.resolve(cliOptions.directory || process.cwd())
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)
}
})
}
}
})
return this
}
/* serve static files */
addStatic (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) {
/* update global cliOptions */
cliOptions.directory = cliOptions.directory || root || process.cwd()
options = Object.assign({ hidden: true }, options)
if (cliOptions.directory) {
const serve = require('koa-static')
return serve(cliOptions.directory, options)
}
}
})
return this
}
/* serve directory index */
addIndex (path, options) {
this.push({
middleware: function (cliOptions) {
path = cliOptions.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
}
}
module.exports = DefaultStack
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
}
})
}