diff --git a/example/stack/cache-control.js b/example/stack/cache-control.js new file mode 100644 index 0000000..52863aa --- /dev/null +++ b/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 diff --git a/lib/cli-data.js b/lib/cli-data.js index e258e0c..b1d4eb2 100644 --- a/lib/cli-data.js +++ b/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.' }, diff --git a/lib/debug.js b/lib/debug.js deleted file mode 100644 index a3122b9..0000000 --- a/lib/debug.js +++ /dev/null @@ -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 -} diff --git a/lib/default-stack.js b/lib/default-stack.js deleted file mode 100644 index ebbf56f..0000000 --- a/lib/default-stack.js +++ /dev/null @@ -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 - } - }) -} diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 792dc65..dd84681 100644 --- a/lib/local-web-server.js +++ b/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 diff --git a/lib/middleware.js b/lib/middleware.js deleted file mode 100644 index 23ce959..0000000 --- a/lib/middleware.js +++ /dev/null @@ -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 - }) - }) - } -} diff --git a/package.json b/package.json index b711b35..ff5a604 100644 --- a/package.json +++ b/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", diff --git a/test/fixture/ajax.html b/test/fixture/ajax.html deleted file mode 100644 index 3430c61..0000000 --- a/test/fixture/ajax.html +++ /dev/null @@ -1,18 +0,0 @@ - - - Ajax test - - -

README

-

loaded in the "Ajax" style

-

-  
-
diff --git a/test/fixture/file.txt b/test/fixture/file.txt
deleted file mode 100644
index 9daeafb..0000000
--- a/test/fixture/file.txt
+++ /dev/null
@@ -1 +0,0 @@
-test
diff --git a/test/fixture/one/file.txt b/test/fixture/one/file.txt
deleted file mode 100644
index 5626abf..0000000
--- a/test/fixture/one/file.txt
+++ /dev/null
@@ -1 +0,0 @@
-one
diff --git a/test/fixture/spa/one.txt b/test/fixture/spa/one.txt
deleted file mode 100644
index 5626abf..0000000
--- a/test/fixture/spa/one.txt
+++ /dev/null
@@ -1 +0,0 @@
-one
diff --git a/test/fixture/spa/two.txt b/test/fixture/spa/two.txt
deleted file mode 100644
index f719efd..0000000
--- a/test/fixture/spa/two.txt
+++ /dev/null
@@ -1 +0,0 @@
-two