diff --git a/example/mock-async/mocks/delayed.js b/example/mock-async/mocks/delayed.js index 0b08823..ec6d325 100644 --- a/example/mock-async/mocks/delayed.js +++ b/example/mock-async/mocks/delayed.js @@ -1,4 +1,5 @@ module.exports = { + name: 'delayed response', response: function (ctx) { return new Promise((resolve, reject) => { setTimeout(() => { diff --git a/example/mock/.local-web-server.json b/example/mock/.local-web-server.json index 736460e..51c31d8 100644 --- a/example/mock/.local-web-server.json +++ b/example/mock/.local-web-server.json @@ -39,7 +39,7 @@ ] }, { - "route": "/four", + "route": "/stream", "module": "/mocks/stream-self.js" }, { diff --git a/example/mock/mocks/five.js b/example/mock/mocks/five.js index 091186b..5165d64 100644 --- a/example/mock/mocks/five.js +++ b/example/mock/mocks/five.js @@ -1,4 +1,5 @@ module.exports = { + name: '/five/:id?name=:name', response: function (ctx, id) { ctx.body = `

id: ${id}, name: ${ctx.query.name}

` } diff --git a/example/mock/mocks/stream-self.js b/example/mock/mocks/stream-self.js index 981fe26..b80018d 100644 --- a/example/mock/mocks/stream-self.js +++ b/example/mock/mocks/stream-self.js @@ -1,6 +1,7 @@ const fs = require('fs') module.exports = { + name: 'stream response', response: { body: fs.createReadStream(__filename) } diff --git a/example/mock/mocks/user.js b/example/mock/mocks/user.js index 1998936..36f4d4a 100644 --- a/example/mock/mocks/user.js +++ b/example/mock/mocks/user.js @@ -7,6 +7,7 @@ const mockResponses = [ /* for GET requests, return a particular user */ { + name: 'GET user', request: { method: 'GET' }, response: function (ctx, id) { ctx.body = users.find(user => user.id === Number(id)) @@ -15,6 +16,7 @@ const mockResponses = [ /* for PUT requests, update the record */ { + name: 'PUT user', request: { method: 'PUT' }, response: function (ctx, id) { const updatedUser = ctx.request.body @@ -26,6 +28,7 @@ const mockResponses = [ /* DELETE request: remove the record */ { + name: 'DELETE user', request: { method: 'DELETE' }, response: function (ctx, id) { const existingUserIndex = users.findIndex(user => user.id === Number(id)) diff --git a/example/spa/.local-web-server.json b/example/spa/.local-web-server.json index 2c63606..d5db15c 100644 --- a/example/spa/.local-web-server.json +++ b/example/spa/.local-web-server.json @@ -1,3 +1,3 @@ { - "spa": "index.html" + "spa": "index.html" } diff --git a/extend/cache-control.js b/extend/cache-control.js index bd33057..d7fb67e 100644 --- a/extend/cache-control.js +++ b/extend/cache-control.js @@ -9,7 +9,7 @@ ws.addLogging('dev') .add({ optionDefinitions: optionDefinitions, middleware: function (options) { - return cacheControl({ maxAge: options.middleware.maxage }) + return cacheControl({ maxAge: options.maxage }) } }) .addStatic() diff --git a/lib/debug.js b/lib/debug.js new file mode 100644 index 0000000..55eca0f --- /dev/null +++ b/lib/debug.js @@ -0,0 +1,11 @@ +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/local-web-server.js b/lib/local-web-server.js index e54afac..297b98d 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -4,67 +4,82 @@ const ansi = require('ansi-escape-sequences') const path = require('path') const arrayify = require('array-back') const t = require('typical') -const Tool = require('command-line-tool') +const CommandLineTool = require('command-line-tool') const MiddlewareStack = require('./middleware-stack') +const debug = require('./debug') -const tool = new Tool() +const tool = new CommandLineTool() + +class LocalWebServer extends MiddlewareStack { + getApplication () { + const Koa = require('koa') + const app = new Koa() + app.use(this.compose(this.options)) + return app + } + + getServer () { + const options = this.options + let key = options.key + let cert = options.cert + + const app = this.getApplication() + app.on('error', err => { + if (options['log-format']) { + console.error(ansi.format(err.message, 'red')) + } + }) + + if (options.https) { + key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') + cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') + } + + let server = null + if (key && cert) { + const fs = require('fs') + const serverOptions = { + key: fs.readFileSync(key), + cert: fs.readFileSync(cert) + } + + const https = require('https') + server = https.createServer(serverOptions, app.callback()) + server.isHttps = true + } else { + const http = require('http') + server = http.createServer(app.callback()) + } + return server + } -class Cli extends MiddlewareStack { start () { const options = collectOptions(this.getOptionDefinitions()) this.options = options - if (options.misc.verbose) { - process.env.DEBUG = '*' + if (options.verbose) { + debug.setLevel(1) } - if (options.misc.config) { + if (options.config) { tool.stop(JSON.stringify(options, null, ' '), 0) } else { - const Koa = require('koa') - const app = new Koa() - app.on('error', err => { - if (options.middleware['log-format']) { - console.error(ansi.format(err.message, 'red')) - } - }) - - app.use(this.compose(options)) - - let key = options.server.key - let cert = options.server.cert - if (options.server.https) { - key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') - cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') - } - - if (key && cert) { - const https = require('https') - const fs = require('fs') - - const serverOptions = { - key: fs.readFileSync(key), - cert: fs.readFileSync(cert) - } - - const server = https.createServer(serverOptions, app.callback()) - server.listen(options.server.port, onServerUp.bind(null, options, true)) - } else { - app.listen(options.server.port, onServerUp.bind(null, options)) - } + const server = this.getServer() + server.listen(options.port, onServerUp.bind(null, options, server.isHttps)) + return server } } } function onServerUp (options, isHttps) { const ipList = getIPList(isHttps) - .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${options.server.port}}`) + .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) .join(', ') console.error(ansi.format( - path.resolve(options.middleware.directory) === process.cwd() + path.resolve(options.directory) === process.cwd() ? `serving at ${ipList}` - : `serving [underline]{${options.middleware.directory}} at ${ipList}` + : `serving [underline]{${options.directory}} at ${ipList}` )) } @@ -90,18 +105,20 @@ function collectOptions (mwOptionDefinitions) { /* parse command line args */ const definitions = cli.optionDefinitions.concat(arrayify(mwOptionDefinitions)) - let options = tool.getOptions(definitions, cli.usage(definitions)) + let cliOptions = tool.getOptions(definitions, cli.usage(definitions)) - /* override built-in defaults with stored config and then command line args */ - options.server = Object.assign({ port: 8000 }, stored.server, options.server) - options.middleware = Object.assign({ directory: process.cwd() }, stored.middleware || {}, options.middleware) + /* override built-in defaults with stored config and then command line options */ + const options = Object.assign({ + port: 8000, + directory: process.cwd() + }, stored, cliOptions.server, cliOptions.middleware, cliOptions.misc) - if (options.middleware.rewrite) { - options.middleware.rewrite = parseRewriteRules(options.middleware.rewrite) + if (options.rewrite) { + options.rewrite = parseRewriteRules(options.rewrite) } + // console.error(require('util').inspect(options, { depth: 3, colors: true })) validateOptions(options) - // console.log(options) return options } @@ -109,6 +126,7 @@ 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] @@ -120,11 +138,15 @@ function parseRewriteRules (rules) { } function validateOptions (options) { - if (!t.isNumber(options.server.port)) { + if (!t.isNumber(options.port)) { tool.printError('--port must be numeric') console.error(tool.usage) tool.halt() } } -module.exports = Cli +module.exports = LocalWebServer + +process.on('unhandledRejection', (reason, p) => { + console.error('unhandledRejection', reason, p) +}) diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index a191d1e..47979c4 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -2,10 +2,11 @@ const arrayify = require('array-back') const path = require('path') const url = require('url') -const debug = require('debug')('local-web-server') +const debug = require('./debug') const mw = require('./middleware') const t = require('typical') const compose = require('koa-compose') +const flatten = require('reduce-flatten') class MiddlewareStack extends Array { add (middleware) { @@ -36,9 +37,9 @@ class MiddlewareStack extends Array { 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 = arrayify(cliOptions.middleware.rewrite || rewriteRules) + const options = arrayify(cliOptions.rewrite || rewriteRules) if (options.length) { - options.forEach(route => { + return options.map(route => { if (route.to) { /* `to` address is remote if the url specifies a host */ if (url.parse(route.to).host) { @@ -75,7 +76,7 @@ class MiddlewareStack extends Array { description: 'A list of forbidden routes.' }, middleware: function (cliOptions) { - forbidList = arrayify(cliOptions.middleware.forbid || forbidList) + forbidList = arrayify(cliOptions.forbid || forbidList) if (forbidList.length) { const pathToRegexp = require('path-to-regexp') debug('forbid', forbidList.join(', ')) @@ -101,7 +102,7 @@ class MiddlewareStack extends Array { description: 'Disable etag-based caching - forces loading from disk each request.' }, middleware: function (cliOptions) { - const noCache = cliOptions.middleware['no-cache'] + const noCache = cliOptions['no-cache'] if (!noCache) { return [ require('koa-conditional-get')(), @@ -117,7 +118,7 @@ class MiddlewareStack extends Array { addMimeType (mime) { this.push({ middleware: function (cliOptions) { - mime = cliOptions.middleware.mime || mime + mime = cliOptions.mime || mime if (mime) { debug('mime override', JSON.stringify(mime)) return mw.mime(mime) @@ -135,8 +136,8 @@ class MiddlewareStack extends Array { description: 'Serve gzip-compressed resources, where applicable.' }, middleware: function (cliOptions) { - compress = t.isDefined(cliOptions.middleware.compress) - ? cliOptions.middleware.compress + compress = t.isDefined(cliOptions.compress) + ? cliOptions.compress : compress if (compress) { debug('compression', 'enabled') @@ -158,9 +159,9 @@ class MiddlewareStack extends Array { 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.middleware['log-format'] || format + format = cliOptions['log-format'] || format - if (cliOptions.misc.verbose && !format) { + if (cliOptions.verbose && !format) { format = 'none' } @@ -190,11 +191,11 @@ class MiddlewareStack extends Array { addMockResponses (mocks) { this.push({ middleware: function (cliOptions) { - mocks = arrayify(cliOptions.middleware.mocks || mocks) - mocks.forEach(mock => { + mocks = arrayify(cliOptions.mocks || mocks) + return mocks.map(mock => { if (mock.module) { - // TODO: ENSURE cliOptions.static.root is correct value - mock.responses = require(path.resolve(path.join(cliOptions.static.root, mock.module))) + const modulePath = path.resolve(path.join(cliOptions.directory, mock.module)) + mock.responses = require(modulePath) } if (mock.responses) { @@ -220,13 +221,13 @@ class MiddlewareStack extends Array { description: 'Path to a Single Page App, e.g. app.html.' }, middleware: function (cliOptions) { - spa = t.isDefined(cliOptions.middleware.spa) ? cliOptions.middleware.spa : spa + spa = t.isDefined(cliOptions.spa) ? cliOptions.spa : spa if (spa) { const historyApiFallback = require('koa-connect-history-api-fallback') debug('SPA', spa) return historyApiFallback({ index: spa, - verbose: cliOptions.misc.verbose + verbose: cliOptions.verbose }) } } @@ -242,12 +243,12 @@ class MiddlewareStack extends Array { description: 'Root directory, defaults to the current directory.' }, middleware: function (cliOptions) { - root = cliOptions.middleware.directory || root || process.cwd() + cliOptions.directory = cliOptions.directory || root || process.cwd() options = Object.assign({ hidden: true }, options) // console.log(root, options, cliOptions) - if (root) { + if (cliOptions.directory) { const serve = require('koa-static') - return serve(root, options) + return serve(cliOptions.directory, options) } } }) @@ -258,7 +259,7 @@ class MiddlewareStack extends Array { addIndex (path, options) { this.push({ middleware: function (cliOptions) { - path = cliOptions.middleware.directory || path || process.cwd() + path = cliOptions.directory || path || process.cwd() options = Object.assign({ icons: true, hidden: true }, options) if (path) { const serveIndex = require('koa-serve-index') @@ -270,7 +271,6 @@ class MiddlewareStack extends Array { } getOptionDefinitions () { - const flatten = require('reduce-flatten') return this .filter(mw => mw.optionDefinitions) .map(mw => mw.optionDefinitions) @@ -283,8 +283,13 @@ class MiddlewareStack extends Array { compose (options) { const convert = require('koa-convert') const middlewareStack = this - .filter(mw => mw) - .map(mw => compose(arrayify(mw.middleware(options)).map(convert))) + .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 })) return compose(middlewareStack) } } diff --git a/lib/middleware.js b/lib/middleware.js index 620780e..2ee450d 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -4,7 +4,7 @@ const url = require('url') const arrayify = require('array-back') const t = require('typical') const pathToRegexp = require('path-to-regexp') -const debug = require('debug')('local-web-server') +const debug = require('./debug') /** * @module middleware @@ -61,8 +61,8 @@ function mime (mimeTypes) { function mockResponses (route, targets) { targets = arrayify(targets) - debug('mock route: %s, targets: %s', route, targets.length) const pathRe = pathToRegexp(route) + debug('mock route: %s, targets: %s', route, targets.length) return function mockResponse (ctx, next) { if (pathRe.test(ctx.path)) { @@ -83,6 +83,8 @@ function mockResponses (route, targets) { target = targets.find(target => !target.request) } + debug(`mock path: ${ctx.path} target: ${target.name || "unnamed"}`) + if (target) { if (t.isFunction(target.response)) { const pathMatches = ctx.path.match(pathRe).slice(1) diff --git a/package.json b/package.json index c0c7fe8..a88acf4 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "array-back": "^1.0.3", "command-line-tool": "75lb/command-line-tool", "config-master": "^2.0.2", - "debug": "^2.2.0", "http-proxy": "^1.13.3", "kcors": "^1.2.1", "koa": "^2.0.0",