Browse Source

added --stack option.. refactor

master
Lloyd Brookes 8 years ago
parent
commit
6440486b0c
  1. 23
      example/stack/cache-control.js
  2. 4
      lib/cli-data.js
  3. 12
      lib/debug.js
  4. 309
      lib/default-stack.js
  5. 109
      lib/local-web-server.js
  6. 59
      lib/middleware.js
  7. 3
      package.json
  8. 18
      test/fixture/ajax.html
  9. 1
      test/fixture/file.txt
  10. 1
      test/fixture/one/file.txt
  11. 1
      test/fixture/spa/one.txt
  12. 1
      test/fixture/spa/two.txt

23
example/stack/cache-control.js

@ -0,0 +1,23 @@
'use strict'
const LocalWebServer = require('../../')
const cacheControl = require('koa-cache-control')
const DefaultStack = require('local-web-server-default-stack')
class CacheControl extends DefaultStack {
addAll () {
this.addLogging('dev')
.add({
optionDefinitions: {
name: 'maxage', type: Number,
description: 'The maxage to set on each response.'
},
middleware: function (options) {
return cacheControl({ maxAge: options.maxage })
}
})
.addStatic()
.addIndex()
}
}
module.exports = CacheControl

4
lib/cli-data.js

@ -4,6 +4,10 @@ exports.optionDefinitions = [
description: 'Web server port.', group: 'server'
},
{
name: 'stack', type: String,
description: 'Middleware stack.', group: 'server'
},
{
name: 'key', type: String, typeLabel: '[underline]{file}', group: 'server',
description: 'SSL key. Supply along with --cert to launch a https server.'
},

12
lib/debug.js

@ -1,12 +0,0 @@
'use strict'
module.exports = debug
let level = 0
function debug () {
if (level) console.error.apply(console.error, arguments)
}
debug.setLevel = function () {
level = 1
}

309
lib/default-stack.js

@ -1,309 +0,0 @@
'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
}
})
}

109
lib/local-web-server.js

@ -5,8 +5,7 @@ const path = require('path')
const arrayify = require('array-back')
const t = require('typical')
const CommandLineTool = require('command-line-tool')
const DefaultStack = require('./default-stack')
const debug = require('./debug')
const DefaultStack = require('local-web-server-default-stack')
/**
* @module local-web-server
@ -20,17 +19,54 @@ const tool = new CommandLineTool()
*/
class LocalWebServer {
constructor (stack) {
this.stack = stack || new DefaultStack()
const commandLineArgs = require('command-line-args')
const commandLineUsage = require('command-line-usage')
const cli = require('../lib/cli-data')
let stackPath
const stackIndex = process.argv.indexOf('--stack')
if (stackIndex > -1) {
stackPath = process.argv[stackIndex + 1]
if (/^-/.test(stackPath)) stackPath = null
}
const stackModule = loadStack(stackPath) || DefaultStack
this.stack = new stackModule()
this.stack.addAll()
const middlewareOptionDefinitions = this.stack.getOptionDefinitions()
// console.log(middlewareOptionDefinitions)
const usage = commandLineUsage(cli.usage(middlewareOptionDefinitions))
let options = {}
try {
options = commandLineArgs(cli.optionDefinitions.concat(middlewareOptionDefinitions))
} catch (err) {
console.error(usage)
tool.halt(err)
}
const loadConfig = require('config-master')
const stored = loadConfig('local-web-server')
/* override stored config with command line options */
options = Object.assign(stored, options.server, options.middleware, options.misc)
this.options = options
if (options.verbose) {
// debug.setLevel(1)
}
if (options.config) {
tool.stop(JSON.stringify(options, null, ' '), 0)
} else if (options.version) {
const pkg = require(path.resolve(__dirname, '..', 'package.json'))
tool.stop(pkg.version)
} else {
if (this.options.help) {
tool.stop(usage)
}
}
}
_init (options) {
this.options = this.options || Object.assign(options || {}, collectUserOptions(this.stack.getOptionDefinitions()))
}
addStack (stack) {
this.stack = stack
}
getApplication (options) {
this._init(options)
const Koa = require('koa')
const app = new Koa()
app.use(this.stack.compose(this.options))
@ -73,27 +109,14 @@ class LocalWebServer {
}
listen (options, callback) {
this._init(options)
options = this.options
if (options.verbose) {
debug.setLevel(1)
}
if (options.config) {
tool.stop(JSON.stringify(options, null, ' '), 0)
} else if (options.version) {
const pkg = require(path.resolve(__dirname, '..', 'package.json'))
tool.stop(pkg.version)
} else {
const server = this.getServer()
const port = options.port || 8000
server.listen(port, () => {
onServerUp(port, options.directory, server.isHttps)
if (callback) callback()
})
return server
}
const server = this.getServer()
const port = options.port || 8000
server.listen(port, () => {
onServerUp(port, options.directory, server.isHttps)
if (callback) callback()
})
return server
}
}
@ -131,14 +154,34 @@ function collectUserOptions (mwOptionDefinitions) {
const cli = require('../lib/cli-data')
/* parse command line args */
const definitions = cli.optionDefinitions.concat(arrayify(mwOptionDefinitions))
const definitions = mwOptionDefinitions
? cli.optionDefinitions.concat(arrayify(mwOptionDefinitions))
: cli.optionDefinitions
let cliOptions = tool.getOptions(definitions, cli.usage(definitions))
/* override stored config with command line options */
const options = Object.assign(stored, cliOptions.server, cliOptions.middleware, cliOptions.misc)
// console.error(require('util').inspect(options, { depth: 3, colors: true }))
return options
}
function loadStack (modulePath) {
let module
if (modulePath) {
const fs = require('fs')
try {
module = require(path.resolve(modulePath))
if (!module.prototype.addAll) {
tool.halt(new Error('Must supply a MiddlewareStack'))
}
} catch (err) {
const walkBack = require('walk-back')
const foundPath = walkBack(path.resolve(process.cwd(), 'node_modules'), modulePath)
if (foundPath) {
module = require(foundPath)
}
}
}
return module
}
module.exports = LocalWebServer

59
lib/middleware.js

@ -1,59 +0,0 @@
'use strict'
const path = require('path')
const url = require('url')
const arrayify = require('array-back')
const t = require('typical')
const pathToRegexp = require('path-to-regexp')
const debug = require('./debug')
/**
* @module middleware
*/
exports.proxyRequest = proxyRequest
exports.mime = mime
function proxyRequest (route) {
const httpProxy = require('http-proxy')
const proxy = httpProxy.createProxyServer({
changeOrigin: true,
secure: false
})
return function proxyMiddleware () {
const keys = []
route.re = pathToRegexp(route.from, keys)
route.new = this.url.replace(route.re, route.to)
keys.forEach((key, index) => {
const re = RegExp(`:${key.name}`, 'g')
route.new = route.new
.replace(re, arguments[index + 1] || '')
})
debug('proxy request', `from: ${this.path}, to: ${url.parse(route.new).href}`)
return new Promise((resolve, reject) => {
proxy.once('error', err => {
err.message = `[PROXY] Error: ${err.message} Target: ${route.new}`
reject(err)
})
proxy.once('proxyReq', function (proxyReq) {
proxyReq.path = url.parse(route.new).path
})
proxy.once('close', resolve)
proxy.web(this.req, this.res, { target: route.new })
})
}
}
function mime (mimeTypes) {
return function mime (ctx, next) {
return next().then(() => {
const reqPathExtension = path.extname(ctx.path).slice(1)
Object.keys(mimeTypes).forEach(mimeType => {
const extsToOverride = mimeTypes[mimeType]
if (extsToOverride.indexOf(reqPathExtension) > -1) ctx.type = mimeType
})
})
}
}

3
package.json

@ -58,7 +58,8 @@
"reduce-flatten": "^1.0.0",
"stream-log-stats": "^1.1.3",
"test-value": "^2.0.0",
"typical": "^2.4.2"
"typical": "^2.4.2",
"walk-back": "^2.0.1"
},
"devDependencies": {
"jsdoc-to-markdown": "^1.3.6",

18
test/fixture/ajax.html

@ -1,18 +0,0 @@
<!DOCTYPE html>
<head>
<title>Ajax test</title>
</head>
<body>
<h1>README</h1>
<h2>loaded in the "Ajax" style</h2>
<pre id="readme"></pre>
<script>
var $ = document.querySelector.bind(document);
var req = new XMLHttpRequest();
req.open("get", "http://localhost:8000/big-file.txt", true);
req.onload = function(){
$("#readme").textContent = this.responseText;
}
req.send()
</script>
</body>

1
test/fixture/file.txt

@ -1 +0,0 @@
test

1
test/fixture/one/file.txt

@ -1 +0,0 @@
one

1
test/fixture/spa/one.txt

@ -1 +0,0 @@
one

1
test/fixture/spa/two.txt

@ -1 +0,0 @@
two
Loading…
Cancel
Save