From 34f245c49d44854fbcbbfb4d5a73edcf4a3f1af7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 18 May 2016 11:35:11 +0100 Subject: [PATCH 001/136] use composite, add error handler --- bin/cli.js | 21 ++++++++++++++- lib/local-web-server.js | 69 +++++++++++++++++++++++++++++++++---------------- lib/middleware.js | 2 +- package.json | 1 + 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 8de7ca6..0ca6e55 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -35,7 +35,24 @@ if (options.misc.help) { return } - const app = localWebServer({ + const convert = require('koa-convert') + const Koa = require('koa') + const app = new Koa() + const _use = app.use + app.use = x => _use.call(app, convert(x)) + + // app.use((ctx, next) => { + // return next() + // .catch(err => { + // console.error('FUKKK', err) + // }) + // }) + + app.on('error', err => { + console.error('ERROROO', err) + }) + + const ws = localWebServer({ static: { root: options.server.directory, options: { @@ -62,6 +79,8 @@ if (options.misc.help) { mocks: options.server.mocks }) + app.use(ws) + if (options.server.https) { options.server.key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') options.server.cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 20539d3..b766dfb 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -74,15 +74,19 @@ function localWebServer (options) { const bodyParser = require('koa-bodyparser') const mw = require('./middleware') - const app = new Koa() - const _use = app.use - app.use = x => _use.call(app, convert(x)) + let middlewares = [] + + // const app = new Koa() + // const _use = app.use + // app.use = x => _use.call(app, convert(x)) /* CORS: allow from any origin */ - app.use(cors()) + // app.use(cors()) + middlewares.push(cors()) /* pretty print JSON */ - app.use(json()) + // app.use(json()) + middlewares.push(json()) /* rewrite rules */ if (options.rewrite && options.rewrite.length) { @@ -91,45 +95,53 @@ function localWebServer (options) { /* `to` address is remote if the url specifies a host */ if (url.parse(route.to).host) { debug('proxy rewrite', `${route.from} -> ${route.to}`) - app.use(_.all(route.from, mw.proxyRequest(route, app))) + // app.use(_.all(route.from, mw.proxyRequest(route))) + middlewares.push(_.all(route.from, mw.proxyRequest(route))) } else { const rewrite = require('koa-rewrite') const rmw = rewrite(route.from, route.to) rmw._name = 'rewrite' - app.use(rmw) + // app.use(rmw) + middlewares.push(rmw) } } }) } /* must come after rewrite. See https://github.com/nodejitsu/node-http-proxy/issues/180. */ - app.use(bodyParser()) + // app.use(bodyParser()) + middlewares.push(bodyParser()) /* path blacklist */ if (options.forbid.length) { debug('forbid', options.forbid.join(', ')) - app.use(mw.blacklist(options.forbid)) + // app.use(mw.blacklist(options.forbid)) + middlewares.push(mw.blacklist(options.forbid)) } /* cache */ if (!options['no-cache']) { const conditional = require('koa-conditional-get') const etag = require('koa-etag') - app.use(conditional()) - app.use(etag()) + // app.use(conditional()) + // app.use(etag()) + middlewares.push(conditional()) + middlewares.push(etag()) } /* mime-type overrides */ if (options.mime) { debug('mime override', JSON.stringify(options.mime)) - app.use(mw.mime(options.mime)) + // app.use(mw.mime(options.mime)) + middlewares.push(mw.mime(options.mime)) } /* compress response */ if (options.compress) { const compress = require('koa-compress') debug('compression', 'enabled') - app.use(compress()) + // app.use(compress()) + middlewares.push(compress()) } /* Logging */ @@ -139,12 +151,15 @@ function localWebServer (options) { if (!log.format) { const streamLogStats = require('stream-log-stats') log.options.stream = streamLogStats({ refreshRate: 500 }) - app.use(morgan('common', log.options)) + // app.use(morgan('common', log.options)) + middlewares.push(morgan('common', log.options)) } else if (log.format === 'logstalgia') { morgan.token('date', logstalgiaDate) - app.use(morgan('combined', log.options)) + // app.use(morgan('combined', log.options)) + middlewares.push(morgan('combined', log.options)) } else { - app.use(morgan(log.format, log.options)) + // app.use(morgan(log.format, log.options)) + middlewares.push(morgan(log.format, log.options)) } } @@ -155,13 +170,15 @@ function localWebServer (options) { } if (mock.responses) { - app.use(mw.mockResponses(mock.route, mock.responses)) + // app.use(mw.mockResponses(mock.route, mock.responses)) + middlewares.push(mw.mockResponses(mock.route, mock.responses)) } else if (mock.response) { mock.target = { request: mock.request, response: mock.response } - app.use(mw.mockResponses(mock.route, mock.target)) + // app.use(mw.mockResponses(mock.route, mock.target)) + middlewares.push(mw.mockResponses(mock.route, mock.target)) } }) @@ -169,7 +186,11 @@ function localWebServer (options) { if (options.spa) { const historyApiFallback = require('koa-connect-history-api-fallback') debug('SPA', options.spa) - app.use(historyApiFallback({ + // app.use(historyApiFallback({ + // index: options.spa, + // verbose: options.verbose + // })) + middlewares.push(historyApiFallback({ index: options.spa, verbose: options.verbose })) @@ -178,16 +199,20 @@ function localWebServer (options) { /* serve static files */ if (options.static.root) { const serve = require('koa-static') - app.use(serve(options.static.root, options.static.options)) + // app.use(serve(options.static.root, options.static.options)) + middlewares.push(serve(options.static.root, options.static.options)) } /* serve directory index */ if (options.serveIndex.path) { const serveIndex = require('koa-serve-index') - app.use(serveIndex(options.serveIndex.path, options.serveIndex.options)) + // app.use(serveIndex(options.serveIndex.path, options.serveIndex.options)) + middlewares.push(serveIndex(options.serveIndex.path, options.serveIndex.options)) } - return app + const compose = require('koa-compose') + middlewares = middlewares.map(convert) + return compose(middlewares) } function logstalgiaDate () { diff --git a/lib/middleware.js b/lib/middleware.js index 559adac..3c53acb 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -15,7 +15,7 @@ exports.blacklist = blacklist exports.mockResponses = mockResponses exports.mime = mime -function proxyRequest (route, app) { +function proxyRequest (route) { const httpProxy = require('http-proxy') const proxy = httpProxy.createProxyServer({ changeOrigin: true diff --git a/package.json b/package.json index 9d5bd03..f2d4516 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "kcors": "^1.2.0", "koa": "^2.0.0", "koa-bodyparser": "^3.0.0", + "koa-compose": "^3.1.0", "koa-compress": "^1.0.9", "koa-conditional-get": "^1.0.3", "koa-connect-history-api-fallback": "^0.3.0", From 0eb5998605c6dfcf5013a2d8a3ff6dbeaa501d34 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 29 May 2016 18:38:12 +0100 Subject: [PATCH 002/136] upgrade command-line-args --- bin/cli.js | 7 +-- lib/cli-options.js | 152 +++++++++++++++++++++++++++++------------------------ package.json | 15 +++--- 3 files changed, 94 insertions(+), 80 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 0ca6e55..23e8554 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -3,6 +3,7 @@ const localWebServer = require('../') const cliOptions = require('../lib/cli-options') const commandLineArgs = require('command-line-args') +const commandLineUsage = require('command-line-usage') const ansi = require('ansi-escape-sequences') const loadConfig = require('config-master') const path = require('path') @@ -11,9 +12,9 @@ const arrayify = require('array-back') const t = require('typical') const flatten = require('reduce-flatten') -const cli = commandLineArgs(cliOptions.definitions) -const usage = cli.getUsage(cliOptions.usageData) +const usage = commandLineUsage(cliOptions.usageData) const stored = loadConfig('local-web-server') + let options let isHttps = false @@ -129,7 +130,7 @@ function collectOptions () { let options = {} /* parse command line args */ - options = cli.parse() + options = commandLineArgs(cliOptions.definitions) const builtIn = { port: 8000, diff --git a/lib/cli-options.js b/lib/cli-options.js index e582ad0..db21ad2 100644 --- a/lib/cli-options.js +++ b/lib/cli-options.js @@ -1,74 +1,86 @@ -module.exports = { - definitions: [ - { - name: 'port', alias: 'p', type: Number, defaultOption: true, - description: 'Web server port.', group: 'server' - }, - { - name: 'directory', alias: 'd', type: String, typeLabel: '[underline]{path}', - description: 'Root directory, defaults to the current directory.', group: 'server' - }, - { - 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').", group: 'server' - }, - { - 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'.", group: 'server' - }, - { - name: 'spa', alias: 's', type: String, typeLabel: '[underline]{file}', - description: 'Path to a Single Page App, e.g. app.html.', group: 'server' - }, - { - name: 'compress', alias: 'c', type: Boolean, - description: 'Serve gzip-compressed resources, where applicable.', group: 'server' - }, - { - name: 'forbid', alias: 'b', type: String, multiple: true, typeLabel: '[underline]{path} ...', - description: 'A list of forbidden routes.', group: 'server' - }, - { - name: 'no-cache', alias: 'n', type: Boolean, - description: 'Disable etag-based caching - forces loading from disk each request.', group: 'server' - }, - { - name: 'key', type: String, typeLabel: '[underline]{file}', group: 'server', - description: 'SSL key. Supply along with --cert to launch a https server.' - }, - { - name: 'cert', type: String, typeLabel: '[underline]{file}', group: 'server', - description: 'SSL cert. Supply along with --key to launch a https server.' - }, - { - name: 'https', type: Boolean, group: 'server', - description: 'Enable HTTPS using a built-in key and cert, registered to the domain 127.0.0.1.' - }, - { - name: 'verbose', type: Boolean, - description: 'Verbose output, useful for debugging.', group: 'server' - }, - { - name: 'help', alias: 'h', type: Boolean, - description: 'Print these usage instructions.', group: 'misc' - }, - { - name: 'config', type: Boolean, - description: 'Print the stored config.', group: 'misc' - } - ], - usageData: { - title: 'local-web-server', - description: 'A simple web-server for productive front-end development.', - footer: 'Project home: [underline]{https://github.com/75lb/local-web-server}', - synopsis: [ +exports.definitions = [ + { + name: 'port', alias: 'p', type: Number, defaultOption: true, + description: 'Web server port.', group: 'server' + }, + { + name: 'directory', alias: 'd', type: String, typeLabel: '[underline]{path}', + description: 'Root directory, defaults to the current directory.', group: 'server' + }, + { + 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').", group: 'server' + }, + { + 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'.", group: 'server' + }, + { + name: 'spa', alias: 's', type: String, typeLabel: '[underline]{file}', + description: 'Path to a Single Page App, e.g. app.html.', group: 'server' + }, + { + name: 'compress', alias: 'c', type: Boolean, + description: 'Serve gzip-compressed resources, where applicable.', group: 'server' + }, + { + name: 'forbid', alias: 'b', type: String, multiple: true, typeLabel: '[underline]{path} ...', + description: 'A list of forbidden routes.', group: 'server' + }, + { + name: 'no-cache', alias: 'n', type: Boolean, + description: 'Disable etag-based caching - forces loading from disk each request.', group: 'server' + }, + { + name: 'key', type: String, typeLabel: '[underline]{file}', group: 'server', + description: 'SSL key. Supply along with --cert to launch a https server.' + }, + { + name: 'cert', type: String, typeLabel: '[underline]{file}', group: 'server', + description: 'SSL cert. Supply along with --key to launch a https server.' + }, + { + name: 'https', type: Boolean, group: 'server', + description: 'Enable HTTPS using a built-in key and cert, registered to the domain 127.0.0.1.' + }, + { + name: 'verbose', type: Boolean, + description: 'Verbose output, useful for debugging.', group: 'server' + }, + { + name: 'help', alias: 'h', type: Boolean, + description: 'Print these usage instructions.', group: 'misc' + }, + { + name: 'config', type: Boolean, + description: 'Print the stored config.', group: 'misc' + } +] + +exports.usageData = [ + { + header: 'local-web-server', + content: 'A simple web-server for productive front-end development.' + }, + { + header: 'Synopsis', + content: [ '$ ws []', '$ ws --config', '$ ws --help' - ], - groups: { - server: 'Server', - misc: 'Misc' - } + ] + }, + { + header: 'Server options', + optionList: exports.definitions, + group: 'server' + }, + { + header: 'Misc options', + optionList: exports.definitions, + group: 'misc' + }, + { + content: 'Project home: [underline]{https://github.com/75lb/local-web-server}' } -} +] diff --git a/package.json b/package.json index f2d4516..ab2c8f4 100644 --- a/package.json +++ b/package.json @@ -31,27 +31,28 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", - "command-line-args": "^2.1.6", + "command-line-args": "^3.0.0", + "command-line-usage": "^3.0.1", "config-master": "^2.0.2", "debug": "^2.2.0", - "http-proxy": "^1.13.2", - "kcors": "^1.2.0", + "http-proxy": "^1.13.3", + "kcors": "^1.2.1", "koa": "^2.0.0", "koa-bodyparser": "^3.0.0", "koa-compose": "^3.1.0", "koa-compress": "^1.0.9", "koa-conditional-get": "^1.0.3", - "koa-connect-history-api-fallback": "^0.3.0", + "koa-connect-history-api-fallback": "~0.3.0", "koa-convert": "^1.2.0", "koa-etag": "^2.1.1", "koa-json": "^1.1.3", "koa-morgan": "^1.0.1", "koa-rewrite": "^2.1.0", - "koa-route": "^3", + "koa-route": "^3.0.0", "koa-send": "^3.2.0", "koa-serve-index": "^1.1.1", "koa-static": "^2.0.0", - "path-to-regexp": "^1.2.1", + "path-to-regexp": "^1.5.0", "reduce-flatten": "^1.0.0", "stream-log-stats": "^1.1.3", "string-tools": "^1.0.0", @@ -60,7 +61,7 @@ }, "devDependencies": { "jsdoc-to-markdown": "^1.3.6", - "req-then": "^0.2.4", + "req-then": "~0.2.4", "tape": "^4.5.1" } } From aa11620b8e0ab27a5a554c0cdeb7ce5f41db07e0 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 30 May 2016 00:32:51 +0100 Subject: [PATCH 003/136] proxy connection errors no longer crash the server, fixes #37 --- bin/cli.js | 11 ++---- example/rewrite/.local-web-server.json | 1 + example/rewrite/index.html | 2 + lib/local-web-server.js | 68 +++++++++++----------------------- lib/middleware.js | 19 +++++----- 5 files changed, 37 insertions(+), 64 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 23e8554..3c358a7 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -42,15 +42,10 @@ if (options.misc.help) { const _use = app.use app.use = x => _use.call(app, convert(x)) - // app.use((ctx, next) => { - // return next() - // .catch(err => { - // console.error('FUKKK', err) - // }) - // }) - app.on('error', err => { - console.error('ERROROO', err) + if (options.server['log-format']) { + console.error(ansi.format(err.message, 'red')) + } }) const ws = localWebServer({ diff --git a/example/rewrite/.local-web-server.json b/example/rewrite/.local-web-server.json index ae46592..1eb9003 100644 --- a/example/rewrite/.local-web-server.json +++ b/example/rewrite/.local-web-server.json @@ -2,6 +2,7 @@ "rewrite": [ { "from": "/css/*", "to": "/build/styles/$1" }, { "from": "/npm/*", "to": "http://registry.npmjs.org/$1" }, + { "from": "/broken/*", "to": "http://localhost:9999" }, { "from": "/:user/repos/:name", "to": "https://api.github.com/repos/:user/:name" } ] } diff --git a/example/rewrite/index.html b/example/rewrite/index.html index 2711101..02dcfd3 100644 --- a/example/rewrite/index.html +++ b/example/rewrite/index.html @@ -9,6 +9,7 @@ "rewrite": [ { "from": "/css/*", "to": "/build/styles/$1" }, { "from": "/npm/*", "to": "http://registry.npmjs.org/$1" }, + { "from": "/broken/*", "to": "http://localhost:9999" }, { "from": "/:user/repos/:name", "to": "https://api.github.com/repos/:user/:name" } ] } @@ -18,5 +19,6 @@ diff --git a/lib/local-web-server.js b/lib/local-web-server.js index b766dfb..8d16a4f 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -66,7 +66,6 @@ function localWebServer (options) { options.mocks = arrayify(options.mocks) const debug = require('debug')('local-web-server') - const Koa = require('koa') const convert = require('koa-convert') const cors = require('kcors') const _ = require('koa-route') @@ -74,19 +73,13 @@ function localWebServer (options) { const bodyParser = require('koa-bodyparser') const mw = require('./middleware') - let middlewares = [] - - // const app = new Koa() - // const _use = app.use - // app.use = x => _use.call(app, convert(x)) + let middlewareStack = [] /* CORS: allow from any origin */ - // app.use(cors()) - middlewares.push(cors()) + middlewareStack.push(cors()) /* pretty print JSON */ - // app.use(json()) - middlewares.push(json()) + middlewareStack.push(json()) /* rewrite rules */ if (options.rewrite && options.rewrite.length) { @@ -95,53 +88,45 @@ function localWebServer (options) { /* `to` address is remote if the url specifies a host */ if (url.parse(route.to).host) { debug('proxy rewrite', `${route.from} -> ${route.to}`) - // app.use(_.all(route.from, mw.proxyRequest(route))) - middlewares.push(_.all(route.from, mw.proxyRequest(route))) + middlewareStack.push(_.all(route.from, mw.proxyRequest(route))) } else { const rewrite = require('koa-rewrite') const rmw = rewrite(route.from, route.to) rmw._name = 'rewrite' - // app.use(rmw) - middlewares.push(rmw) + middlewareStack.push(rmw) } } }) } /* must come after rewrite. See https://github.com/nodejitsu/node-http-proxy/issues/180. */ - // app.use(bodyParser()) - middlewares.push(bodyParser()) + middlewareStack.push(bodyParser()) /* path blacklist */ if (options.forbid.length) { debug('forbid', options.forbid.join(', ')) - // app.use(mw.blacklist(options.forbid)) - middlewares.push(mw.blacklist(options.forbid)) + middlewareStack.push(mw.blacklist(options.forbid)) } /* cache */ if (!options['no-cache']) { const conditional = require('koa-conditional-get') const etag = require('koa-etag') - // app.use(conditional()) - // app.use(etag()) - middlewares.push(conditional()) - middlewares.push(etag()) + middlewareStack.push(conditional()) + middlewareStack.push(etag()) } /* mime-type overrides */ if (options.mime) { debug('mime override', JSON.stringify(options.mime)) - // app.use(mw.mime(options.mime)) - middlewares.push(mw.mime(options.mime)) + middlewareStack.push(mw.mime(options.mime)) } /* compress response */ if (options.compress) { const compress = require('koa-compress') debug('compression', 'enabled') - // app.use(compress()) - middlewares.push(compress()) + middlewareStack.push(compress()) } /* Logging */ @@ -151,15 +136,12 @@ function localWebServer (options) { if (!log.format) { const streamLogStats = require('stream-log-stats') log.options.stream = streamLogStats({ refreshRate: 500 }) - // app.use(morgan('common', log.options)) - middlewares.push(morgan('common', log.options)) + middlewareStack.push(morgan('common', log.options)) } else if (log.format === 'logstalgia') { morgan.token('date', logstalgiaDate) - // app.use(morgan('combined', log.options)) - middlewares.push(morgan('combined', log.options)) + middlewareStack.push(morgan('combined', log.options)) } else { - // app.use(morgan(log.format, log.options)) - middlewares.push(morgan(log.format, log.options)) + middlewareStack.push(morgan(log.format, log.options)) } } @@ -170,15 +152,13 @@ function localWebServer (options) { } if (mock.responses) { - // app.use(mw.mockResponses(mock.route, mock.responses)) - middlewares.push(mw.mockResponses(mock.route, mock.responses)) + middlewareStack.push(mw.mockResponses(mock.route, mock.responses)) } else if (mock.response) { mock.target = { request: mock.request, response: mock.response } - // app.use(mw.mockResponses(mock.route, mock.target)) - middlewares.push(mw.mockResponses(mock.route, mock.target)) + middlewareStack.push(mw.mockResponses(mock.route, mock.target)) } }) @@ -186,11 +166,7 @@ function localWebServer (options) { if (options.spa) { const historyApiFallback = require('koa-connect-history-api-fallback') debug('SPA', options.spa) - // app.use(historyApiFallback({ - // index: options.spa, - // verbose: options.verbose - // })) - middlewares.push(historyApiFallback({ + middlewareStack.push(historyApiFallback({ index: options.spa, verbose: options.verbose })) @@ -199,20 +175,18 @@ function localWebServer (options) { /* serve static files */ if (options.static.root) { const serve = require('koa-static') - // app.use(serve(options.static.root, options.static.options)) - middlewares.push(serve(options.static.root, options.static.options)) + middlewareStack.push(serve(options.static.root, options.static.options)) } /* serve directory index */ if (options.serveIndex.path) { const serveIndex = require('koa-serve-index') - // app.use(serveIndex(options.serveIndex.path, options.serveIndex.options)) - middlewares.push(serveIndex(options.serveIndex.path, options.serveIndex.options)) + middlewareStack.push(serveIndex(options.serveIndex.path, options.serveIndex.options)) } const compose = require('koa-compose') - middlewares = middlewares.map(convert) - return compose(middlewares) + middlewareStack = middlewareStack.map(convert) + return compose(middlewareStack) } function logstalgiaDate () { diff --git a/lib/middleware.js b/lib/middleware.js index 3c53acb..984b768 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -22,7 +22,6 @@ function proxyRequest (route) { }) return function proxyMiddleware () { - const next = arguments[arguments.length - 1] const keys = [] route.re = pathToRegexp(route.from, keys) route.new = this.url.replace(route.re, route.to) @@ -33,17 +32,19 @@ function proxyRequest (route) { .replace(re, arguments[index + 1] || '') }) - this.response = false debug('proxy request', `from: ${this.path}, to: ${url.parse(route.new).href}`) - proxy.once('error', err => { - this.throw(500, `[PROXY] ${err.message}: ${route.new}`) - }) - proxy.once('proxyReq', function (proxyReq) { - proxyReq.path = url.parse(route.new).path + 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 }) }) - - proxy.web(this.req, this.res, { target: route.new }) } } From 1c57d29b3fcbec01e9f68fdfe5cbb4955baf467c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=9A=D1=83=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Mon, 18 Apr 2016 14:08:28 +0300 Subject: [PATCH 004/136] cache-control config --- bin/cli.js | 2 +- lib/local-web-server.js | 8 ++++++++ package.json | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bin/cli.js b/bin/cli.js index 3c358a7..81630b8 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -46,7 +46,6 @@ if (options.misc.help) { if (options.server['log-format']) { console.error(ansi.format(err.message, 'red')) } - }) const ws = localWebServer({ static: { @@ -65,6 +64,7 @@ if (options.misc.help) { log: { format: options.server['log-format'] }, + cacheControl: options.server.cacheControl, compress: options.server.compress, mime: options.server.mime, forbid: options.server.forbid, diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 8d16a4f..60ffaa9 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -38,6 +38,7 @@ function localWebServer (options) { options = Object.assign({ static: {}, serveIndex: {}, + cacheControl: {}, spa: null, log: {}, compress: false, @@ -116,6 +117,13 @@ function localWebServer (options) { middlewareStack.push(etag()) } + /* cache-control headers */ + if (options.cacheControl) { + const cacheControl = require('koa-cache-control') + debug('Cache control', JSON.stringify(options.cacheControl)) + app.use(cacheControl(options.cacheControl)) + } + /* mime-type overrides */ if (options.mime) { debug('mime override', JSON.stringify(options.mime)) diff --git a/package.json b/package.json index ab2c8f4..13438ec 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "koa": "^2.0.0", "koa-bodyparser": "^3.0.0", "koa-compose": "^3.1.0", + "koa-cache-control": "^1.0.0", "koa-compress": "^1.0.9", "koa-conditional-get": "^1.0.3", "koa-connect-history-api-fallback": "~0.3.0", From 6a2bbafd08e56a747d510a29ad82b364d3d0a850 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 30 May 2016 13:48:45 +0100 Subject: [PATCH 005/136] cache-control extension example --- bin/cli.js | 7 ++----- extend/cache-control.js | 17 +++++++++++++++++ lib/local-web-server.js | 7 ------- package.json | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 extend/cache-control.js diff --git a/bin/cli.js b/bin/cli.js index 81630b8..bf500ab 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -13,7 +13,6 @@ const t = require('typical') const flatten = require('reduce-flatten') const usage = commandLineUsage(cliOptions.usageData) -const stored = loadConfig('local-web-server') let options let isHttps = false @@ -36,16 +35,14 @@ if (options.misc.help) { return } - const convert = require('koa-convert') const Koa = require('koa') const app = new Koa() - const _use = app.use - app.use = x => _use.call(app, convert(x)) app.on('error', err => { if (options.server['log-format']) { console.error(ansi.format(err.message, 'red')) } + }) const ws = localWebServer({ static: { @@ -64,7 +61,6 @@ if (options.misc.help) { log: { format: options.server['log-format'] }, - cacheControl: options.server.cacheControl, compress: options.server.compress, mime: options.server.mime, forbid: options.server.forbid, @@ -122,6 +118,7 @@ function onServerUp () { } function collectOptions () { + const stored = loadConfig('local-web-server') let options = {} /* parse command line args */ diff --git a/extend/cache-control.js b/extend/cache-control.js new file mode 100644 index 0000000..8b2fc1d --- /dev/null +++ b/extend/cache-control.js @@ -0,0 +1,17 @@ +'use strict' +const Koa = require('koa') +const localWebServer = require('../') +const cacheControl = require('koa-cache-control') +const convert = require('koa-convert') + +const app = new Koa() +const ws = localWebServer({ + 'no-cache': true, + log: { format: 'dev' } +}) + +app.use(convert(cacheControl({ + maxAge: 15 +}))) +app.use(ws) +app.listen(8000) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 60ffaa9..c5c1d1a 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -117,13 +117,6 @@ function localWebServer (options) { middlewareStack.push(etag()) } - /* cache-control headers */ - if (options.cacheControl) { - const cacheControl = require('koa-cache-control') - debug('Cache control', JSON.stringify(options.cacheControl)) - app.use(cacheControl(options.cacheControl)) - } - /* mime-type overrides */ if (options.mime) { debug('mime override', JSON.stringify(options.mime)) diff --git a/package.json b/package.json index 13438ec..f5361be 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "koa": "^2.0.0", "koa-bodyparser": "^3.0.0", "koa-compose": "^3.1.0", - "koa-cache-control": "^1.0.0", "koa-compress": "^1.0.9", "koa-conditional-get": "^1.0.3", "koa-connect-history-api-fallback": "~0.3.0", @@ -62,6 +61,7 @@ }, "devDependencies": { "jsdoc-to-markdown": "^1.3.6", + "koa-cache-control": "^1.0.0", "req-then": "~0.2.4", "tape": "^4.5.1" } From eee69238583a66f00c2468eefd212720222f2ac3 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 30 May 2016 16:45:43 +0100 Subject: [PATCH 006/136] refactor cli --- bin/cli.js | 175 +++++++++++++++++++----------------- extend/live-reload.js | 14 +++ lib/{cli-options.js => cli-data.js} | 0 lib/local-web-server.js | 7 +- package.json | 1 + 5 files changed, 112 insertions(+), 85 deletions(-) create mode 100644 extend/live-reload.js rename lib/{cli-options.js => cli-data.js} (100%) diff --git a/bin/cli.js b/bin/cli.js index bf500ab..802c487 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,97 +1,91 @@ #!/usr/bin/env node 'use strict' -const localWebServer = require('../') -const cliOptions = require('../lib/cli-options') -const commandLineArgs = require('command-line-args') +const cli = require('../lib/cli-data') const commandLineUsage = require('command-line-usage') const ansi = require('ansi-escape-sequences') -const loadConfig = require('config-master') const path = require('path') -const os = require('os') const arrayify = require('array-back') const t = require('typical') -const flatten = require('reduce-flatten') -const usage = commandLineUsage(cliOptions.usageData) +function ws () { + const usage = commandLineUsage(cli.usageData) -let options -let isHttps = false + let options -try { - options = collectOptions() -} catch (err) { - stop([ `[red]{Error}: ${err.message}`, usage ], 1) - return -} - -if (options.misc.help) { - stop(usage, 0) -} else if (options.misc.config) { - stop(JSON.stringify(options.server, null, ' '), 0) -} else { - const valid = validateOptions(options) - if (!valid) { - /* gracefully end the process */ + try { + options = collectOptions() + } catch (err) { + stop([ `[red]{Error}: ${err.message}`, usage ], 1) return } - const Koa = require('koa') - const app = new Koa() + if (options.misc.help) { + stop(usage, 0) + } else if (options.misc.config) { + stop(JSON.stringify(options.server, null, ' '), 0) + } else { + const localWebServer = require('../') + const Koa = require('koa') - app.on('error', err => { - if (options.server['log-format']) { - console.error(ansi.format(err.message, 'red')) - } - }) + const valid = validateOptions(options, usage) + if (!valid) return - const ws = localWebServer({ - static: { - root: options.server.directory, - options: { - hidden: true - } - }, - serveIndex: { - path: options.server.directory, - options: { - icons: true, - hidden: true - } - }, - log: { - format: options.server['log-format'] - }, - compress: options.server.compress, - mime: options.server.mime, - forbid: options.server.forbid, - spa: options.server.spa, - 'no-cache': options.server['no-cache'], - rewrite: options.server.rewrite, - verbose: options.server.verbose, - mocks: options.server.mocks - }) + const app = new Koa() - app.use(ws) + app.on('error', err => { + if (options.server['log-format']) { + console.error(ansi.format(err.message, 'red')) + } + }) + + const ws = localWebServer({ + static: { + root: options.server.directory, + options: { + hidden: true + } + }, + serveIndex: { + path: options.server.directory, + options: { + icons: true, + hidden: true + } + }, + log: { + format: options.server['log-format'] + }, + compress: options.server.compress, + mime: options.server.mime, + forbid: options.server.forbid, + spa: options.server.spa, + 'no-cache': options.server['no-cache'], + rewrite: options.server.rewrite, + verbose: options.server.verbose, + mocks: options.server.mocks + }) + + app.use(ws) + + if (options.server.https) { + options.server.key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') + options.server.cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') + } - if (options.server.https) { - options.server.key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') - options.server.cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') - } + if (options.server.key && options.server.cert) { + const https = require('https') + const fs = require('fs') - if (options.server.key && options.server.cert) { - const https = require('https') - const fs = require('fs') - isHttps = true + const serverOptions = { + key: fs.readFileSync(options.server.key), + cert: fs.readFileSync(options.server.cert) + } - const serverOptions = { - key: fs.readFileSync(options.server.key), - cert: fs.readFileSync(options.server.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 = https.createServer(serverOptions, app.callback()) - server.listen(options.server.port, onServerUp) - } else { - app.listen(options.server.port, onServerUp) } } @@ -100,13 +94,8 @@ function stop (msgs, exitCode) { process.exitCode = exitCode } -function onServerUp () { - let ipList = Object.keys(os.networkInterfaces()) - .map(key => os.networkInterfaces()[key]) - .reduce(flatten, []) - .filter(iface => iface.family === 'IPv4') - ipList.unshift({ address: os.hostname() }) - ipList = ipList +function onServerUp (options, isHttps) { + const ipList = getIPList(isHttps) .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${options.server.port}}`) .join(', ') @@ -117,12 +106,28 @@ function onServerUp () { )) } +function getIPList (isHttps) { + const flatten = require('reduce-flatten') + const os = require('os') + + let ipList = Object.keys(os.networkInterfaces()) + .map(key => os.networkInterfaces()[key]) + .reduce(flatten, []) + .filter(iface => iface.family === 'IPv4') + ipList.unshift({ address: os.hostname() }) + return ipList +} + +/** + * Return default, stored and command-line options combined + */ function collectOptions () { + const commandLineArgs = require('command-line-args') + const loadConfig = require('config-master') const stored = loadConfig('local-web-server') - let options = {} /* parse command line args */ - options = commandLineArgs(cliOptions.definitions) + let options = commandLineArgs(cli.definitions) const builtIn = { port: 8000, @@ -150,7 +155,7 @@ function parseRewriteRules (rules) { }) } -function validateOptions (options) { +function validateOptions (options, usage) { let valid = true function invalid (msg) { return `[red underline]{Invalid:} [bold]{${msg}}` @@ -162,3 +167,5 @@ function validateOptions (options) { } return valid } + +ws() diff --git a/extend/live-reload.js b/extend/live-reload.js new file mode 100644 index 0000000..6100fb5 --- /dev/null +++ b/extend/live-reload.js @@ -0,0 +1,14 @@ +'use strict' +const Koa = require('koa') +const localWebServer = require('../') +const liveReload = require('koa-livereload') +const convert = require('koa-convert') + +const app = new Koa() +const ws = localWebServer({ + log: { format: 'dev' } +}) + +app.use(liveReload()) +app.use(ws) +app.listen(8000) diff --git a/lib/cli-options.js b/lib/cli-data.js similarity index 100% rename from lib/cli-options.js rename to lib/cli-data.js diff --git a/lib/local-web-server.js b/lib/local-web-server.js index c5c1d1a..51b4a10 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -37,7 +37,12 @@ module.exports = localWebServer function localWebServer (options) { options = Object.assign({ static: {}, - serveIndex: {}, + serveIndex: { + options: { + icons: true, + hidden: true + } + }, cacheControl: {}, spa: null, log: {}, diff --git a/package.json b/package.json index f5361be..cad8837 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "devDependencies": { "jsdoc-to-markdown": "^1.3.6", "koa-cache-control": "^1.0.0", + "koa-livereload": "^0.1.23", "req-then": "~0.2.4", "tape": "^4.5.1" } From bd182e4ffd64e4250af95bd96a5beb92bdc61fa1 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 8 Jun 2016 23:06:41 +0100 Subject: [PATCH 007/136] refactor --- bin/cli.js | 187 +++------------------------ extend/cache-control.js | 27 ++-- lib/cli-data.js | 8 +- lib/local-web-server.js | 327 +++++++++++++++++++----------------------------- lib/middleware-stack.js | 211 +++++++++++++++++++++++++++++++ package.json | 3 +- 6 files changed, 379 insertions(+), 384 deletions(-) create mode 100644 lib/middleware-stack.js diff --git a/bin/cli.js b/bin/cli.js index 802c487..c00b81c 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,171 +1,20 @@ #!/usr/bin/env node 'use strict' -const cli = require('../lib/cli-data') -const commandLineUsage = require('command-line-usage') -const ansi = require('ansi-escape-sequences') -const path = require('path') -const arrayify = require('array-back') -const t = require('typical') - -function ws () { - const usage = commandLineUsage(cli.usageData) - - let options - - try { - options = collectOptions() - } catch (err) { - stop([ `[red]{Error}: ${err.message}`, usage ], 1) - return - } - - if (options.misc.help) { - stop(usage, 0) - } else if (options.misc.config) { - stop(JSON.stringify(options.server, null, ' '), 0) - } else { - const localWebServer = require('../') - const Koa = require('koa') - - const valid = validateOptions(options, usage) - if (!valid) return - - const app = new Koa() - - app.on('error', err => { - if (options.server['log-format']) { - console.error(ansi.format(err.message, 'red')) - } - }) - - const ws = localWebServer({ - static: { - root: options.server.directory, - options: { - hidden: true - } - }, - serveIndex: { - path: options.server.directory, - options: { - icons: true, - hidden: true - } - }, - log: { - format: options.server['log-format'] - }, - compress: options.server.compress, - mime: options.server.mime, - forbid: options.server.forbid, - spa: options.server.spa, - 'no-cache': options.server['no-cache'], - rewrite: options.server.rewrite, - verbose: options.server.verbose, - mocks: options.server.mocks - }) - - app.use(ws) - - if (options.server.https) { - options.server.key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') - options.server.cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') - } - - if (options.server.key && options.server.cert) { - const https = require('https') - const fs = require('fs') - - const serverOptions = { - key: fs.readFileSync(options.server.key), - cert: fs.readFileSync(options.server.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)) - } - } -} - -function stop (msgs, exitCode) { - arrayify(msgs).forEach(msg => console.error(ansi.format(msg))) - process.exitCode = exitCode -} - -function onServerUp (options, isHttps) { - const ipList = getIPList(isHttps) - .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${options.server.port}}`) - .join(', ') - - console.error(ansi.format( - path.resolve(options.server.directory) === process.cwd() - ? `serving at ${ipList}` - : `serving [underline]{${options.server.directory}} at ${ipList}` - )) -} - -function getIPList (isHttps) { - const flatten = require('reduce-flatten') - const os = require('os') - - let ipList = Object.keys(os.networkInterfaces()) - .map(key => os.networkInterfaces()[key]) - .reduce(flatten, []) - .filter(iface => iface.family === 'IPv4') - ipList.unshift({ address: os.hostname() }) - return ipList -} - -/** - * Return default, stored and command-line options combined - */ -function collectOptions () { - const commandLineArgs = require('command-line-args') - const loadConfig = require('config-master') - const stored = loadConfig('local-web-server') - - /* parse command line args */ - let options = commandLineArgs(cli.definitions) - - const builtIn = { - port: 8000, - directory: process.cwd(), - forbid: [], - rewrite: [] - } - - if (options.server.rewrite) { - options.server.rewrite = parseRewriteRules(options.server.rewrite) - } - - /* override built-in defaults with stored config and then command line args */ - options.server = Object.assign(builtIn, stored, options.server) - return options -} - -function parseRewriteRules (rules) { - return rules && rules.map(rule => { - const matches = rule.match(/(\S*)\s*->\s*(\S*)/) - return { - from: matches[1], - to: matches[2] - } - }) -} - -function validateOptions (options, usage) { - let valid = true - function invalid (msg) { - return `[red underline]{Invalid:} [bold]{${msg}}` - } - - if (!t.isNumber(options.server.port)) { - stop([ invalid(`--port must be numeric`), usage ], 1) - valid = false - } - return valid -} - -ws() +const Cli = require('../') + +const ws = new Cli() +ws.middleware.addCors() +ws.middleware.addJson() +ws.middleware.addRewrite() +ws.middleware.addBodyParser() +ws.middleware.addBlacklist() +ws.middleware.addCache() +ws.middleware.addMimeType() +ws.middleware.addCompression() +ws.middleware.addLogging() +ws.middleware.addMockResponses() +ws.middleware.addSpa() + +ws.middleware.addStatic() +ws.middleware.addIndex() +ws.listen() diff --git a/extend/cache-control.js b/extend/cache-control.js index 8b2fc1d..d1c56dc 100644 --- a/extend/cache-control.js +++ b/extend/cache-control.js @@ -1,17 +1,24 @@ 'use strict' -const Koa = require('koa') -const localWebServer = require('../') +const Cli = require('../cli') const cacheControl = require('koa-cache-control') -const convert = require('koa-convert') +const cliData = require('../lib/cli-data') -const app = new Koa() -const ws = localWebServer({ +cliData.push({ name: 'black' }) + +const ws = new Cli({ 'no-cache': true, log: { format: 'dev' } }) -app.use(convert(cacheControl({ - maxAge: 15 -}))) -app.use(ws) -app.listen(8000) +ws.middleware.splice( + ws.middleware.findIndex(m => m.name === 'mime-type'), + 1, + { + name: 'cache-control', + create: convert(cacheControl({ + maxAge: 15 + })) + } +) + +ws.listen() diff --git a/lib/cli-data.js b/lib/cli-data.js index db21ad2..bcd5fd6 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -1,4 +1,4 @@ -exports.definitions = [ +exports.optionDefinitions = [ { name: 'port', alias: 'p', type: Number, defaultOption: true, description: 'Web server port.', group: 'server' @@ -57,7 +57,7 @@ exports.definitions = [ } ] -exports.usageData = [ +exports.usage = [ { header: 'local-web-server', content: 'A simple web-server for productive front-end development.' @@ -72,12 +72,12 @@ exports.usageData = [ }, { header: 'Server options', - optionList: exports.definitions, + optionList: exports.optionDefinitions, group: 'server' }, { header: 'Misc options', - optionList: exports.definitions, + optionList: exports.optionDefinitions, group: 'misc' }, { diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 51b4a10..8d76b36 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,229 +1,158 @@ +#!/usr/bin/env node 'use strict' +const ansi = require('ansi-escape-sequences') const path = require('path') -const url = require('url') const arrayify = require('array-back') +const t = require('typical') +const Tool = require('command-line-tool') +const tool = new Tool() -/** - * @module local-web-server - */ -module.exports = localWebServer +class Cli { + constructor () { + this.options = null + this.app = null + this.middleware = null -/** - * Returns a Koa application you can launch or mix into an existing app. - * - * @param [options] {object} - options - * @param [options.static] {object} - koa-static config - * @param [options.static.root=.] {string} - root directory - * @param [options.static.options] {string} - [options](https://github.com/koajs/static#options) - * @param [options.serveIndex] {object} - koa-serve-index config - * @param [options.serveIndex.path=.] {string} - root directory - * @param [options.serveIndex.options] {string} - [options](https://github.com/expressjs/serve-index#options) - * @param [options.forbid] {string[]} - A list of forbidden routes, each route being an [express route-path](http://expressjs.com/guide/routing.html#route-paths). - * @param [options.spa] {string} - specify an SPA file to catch requests for everything but static assets. - * @param [options.log] {object} - [morgan](https://github.com/expressjs/morgan) config - * @param [options.log.format] {string} - [log format](https://github.com/expressjs/morgan#predefined-formats) - * @param [options.log.options] {object} - [options](https://github.com/expressjs/morgan#options) - * @param [options.compress] {boolean} - Serve gzip-compressed resources, where applicable - * @param [options.mime] {object} - A list of mime-type overrides, passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine) - * @param [options.rewrite] {module:local-web-server~rewriteRule[]} - One or more rewrite rules - * @param [options.verbose] {boolean} - Print detailed output, useful for debugging - * - * @alias module:local-web-server - * @return {external:KoaApplication} - * @example - * const localWebServer = require('local-web-server') - * localWebServer().listen(8000) - */ -function localWebServer (options) { - options = Object.assign({ - static: {}, - serveIndex: { - options: { - icons: true, - hidden: true - } - }, - cacheControl: {}, - spa: null, - log: {}, - compress: false, - mime: {}, - forbid: [], - rewrite: [], - verbose: false, - mocks: [] - }, options) + let options = collectOptions() + this.options = options - if (options.verbose) { - process.env.DEBUG = '*' + if (options.misc.config) { + tool.stop(JSON.stringify(options.server, null, ' '), 0) + } else { + const Koa = require('koa') + const app = new Koa() + this.app = app + + const MiddlewareStack = require('./middleware-stack') + this.middleware = new MiddlewareStack({ + static: { + root: options.server.directory, + options: { + hidden: true + } + }, + serveIndex: { + path: options.server.directory, + options: { + icons: true, + hidden: true + } + }, + log: { + format: options.server['log-format'] + }, + compress: options.server.compress, + mime: options.server.mime, + forbid: options.server.forbid, + spa: options.server.spa, + 'no-cache': options.server['no-cache'], + rewrite: options.server.rewrite, + verbose: options.server.verbose, + mocks: options.server.mocks + }) + + app.on('error', err => { + if (options.server['log-format']) { + console.error(ansi.format(err.message, 'red')) + } + }) + } } - const log = options.log - log.options = log.options || {} + listen () { + this.app.use(this.middleware.getMiddleware()) + const options = this.options + if (options.server.https) { + options.server.key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') + options.server.cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') + } - if (options.verbose && !log.format) { - log.format = 'none' - } + if (options.server.key && options.server.cert) { + const https = require('https') + const fs = require('fs') - if (!options.static.root) options.static.root = process.cwd() - if (!options.serveIndex.path) options.serveIndex.path = process.cwd() - options.rewrite = arrayify(options.rewrite) - options.forbid = arrayify(options.forbid) - options.mocks = arrayify(options.mocks) - - const debug = require('debug')('local-web-server') - const convert = require('koa-convert') - const cors = require('kcors') - const _ = require('koa-route') - const json = require('koa-json') - const bodyParser = require('koa-bodyparser') - const mw = require('./middleware') - - let middlewareStack = [] - - /* CORS: allow from any origin */ - middlewareStack.push(cors()) - - /* pretty print JSON */ - middlewareStack.push(json()) - - /* rewrite rules */ - if (options.rewrite && options.rewrite.length) { - options.rewrite.forEach(route => { - if (route.to) { - /* `to` address is remote if the url specifies a host */ - if (url.parse(route.to).host) { - debug('proxy rewrite', `${route.from} -> ${route.to}`) - middlewareStack.push(_.all(route.from, mw.proxyRequest(route))) - } else { - const rewrite = require('koa-rewrite') - const rmw = rewrite(route.from, route.to) - rmw._name = 'rewrite' - middlewareStack.push(rmw) - } + const serverOptions = { + key: fs.readFileSync(options.server.key), + cert: fs.readFileSync(options.server.cert) } - }) - } - - /* must come after rewrite. See https://github.com/nodejitsu/node-http-proxy/issues/180. */ - middlewareStack.push(bodyParser()) - /* path blacklist */ - if (options.forbid.length) { - debug('forbid', options.forbid.join(', ')) - middlewareStack.push(mw.blacklist(options.forbid)) + const server = https.createServer(serverOptions, this.app.callback()) + server.listen(options.server.port, onServerUp.bind(null, options, true)) + } else { + this.app.listen(options.server.port, onServerUp.bind(null, options)) + } } +} - /* cache */ - if (!options['no-cache']) { - const conditional = require('koa-conditional-get') - const etag = require('koa-etag') - middlewareStack.push(conditional()) - middlewareStack.push(etag()) - } +function onServerUp (options, isHttps) { + const ipList = getIPList(isHttps) + .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${options.server.port}}`) + .join(', ') - /* mime-type overrides */ - if (options.mime) { - debug('mime override', JSON.stringify(options.mime)) - middlewareStack.push(mw.mime(options.mime)) - } + console.error(ansi.format( + path.resolve(options.server.directory) === process.cwd() + ? `serving at ${ipList}` + : `serving [underline]{${options.server.directory}} at ${ipList}` + )) +} - /* compress response */ - if (options.compress) { - const compress = require('koa-compress') - debug('compression', 'enabled') - middlewareStack.push(compress()) - } +function getIPList (isHttps) { + const flatten = require('reduce-flatten') + const os = require('os') - /* Logging */ - if (log.format !== 'none') { - const morgan = require('koa-morgan') - - if (!log.format) { - const streamLogStats = require('stream-log-stats') - log.options.stream = streamLogStats({ refreshRate: 500 }) - middlewareStack.push(morgan('common', log.options)) - } else if (log.format === 'logstalgia') { - morgan.token('date', logstalgiaDate) - middlewareStack.push(morgan('combined', log.options)) - } else { - middlewareStack.push(morgan(log.format, log.options)) - } - } + let ipList = Object.keys(os.networkInterfaces()) + .map(key => os.networkInterfaces()[key]) + .reduce(flatten, []) + .filter(iface => iface.family === 'IPv4') + ipList.unshift({ address: os.hostname() }) + return ipList +} - /* Mock Responses */ - options.mocks.forEach(mock => { - if (mock.module) { - mock.responses = require(path.resolve(path.join(options.static.root, mock.module))) - } +/** + * Return default, stored and command-line options combined + */ +function collectOptions () { + const loadConfig = require('config-master') + const stored = loadConfig('local-web-server') + const cli = require('../lib/cli-data') - if (mock.responses) { - middlewareStack.push(mw.mockResponses(mock.route, mock.responses)) - } else if (mock.response) { - mock.target = { - request: mock.request, - response: mock.response - } - middlewareStack.push(mw.mockResponses(mock.route, mock.target)) - } - }) + /* parse command line args */ + let options = tool.getOptions(cli.optionDefinitions, cli.usage) - /* for any URL not matched by static (e.g. `/search`), serve the SPA */ - if (options.spa) { - const historyApiFallback = require('koa-connect-history-api-fallback') - debug('SPA', options.spa) - middlewareStack.push(historyApiFallback({ - index: options.spa, - verbose: options.verbose - })) + const builtIn = { + port: 8000, + directory: process.cwd(), + forbid: [], + rewrite: [] } - /* serve static files */ - if (options.static.root) { - const serve = require('koa-static') - middlewareStack.push(serve(options.static.root, options.static.options)) + if (options.server.rewrite) { + options.server.rewrite = parseRewriteRules(options.server.rewrite) } - /* serve directory index */ - if (options.serveIndex.path) { - const serveIndex = require('koa-serve-index') - middlewareStack.push(serveIndex(options.serveIndex.path, options.serveIndex.options)) - } + /* override built-in defaults with stored config and then command line args */ + options.server = Object.assign(builtIn, stored, options.server) - const compose = require('koa-compose') - middlewareStack = middlewareStack.map(convert) - return compose(middlewareStack) + validateOptions(options) + return options } -function logstalgiaDate () { - var d = new Date() - return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '') +function parseRewriteRules (rules) { + return rules && rules.map(rule => { + const matches = rule.match(/(\S*)\s*->\s*(\S*)/) + return { + from: matches[1], + to: matches[2] + } + }) } -process.on('unhandledRejection', (reason, p) => { - throw reason -}) - -/** - * The `from` and `to` routes are specified using [express route-paths](http://expressjs.com/guide/routing.html#route-paths) - * - * @example - * ```json - * { - * "rewrite": [ - * { "from": "/css/*", "to": "/build/styles/$1" }, - * { "from": "/npm/*", "to": "http://registry.npmjs.org/$1" }, - * { "from": "/:user/repos/:name", "to": "https://api.github.com/repos/:user/:name" } - * ] - * } - * ``` - * - * @typedef rewriteRule - * @property from {string} - request route - * @property to {string} - target route - */ +function validateOptions (options) { + if (!t.isNumber(options.server.port)) { + tool.printError('--port must be numeric') + console.error(tool.usage) + tool.halt() + } +} -/** - * @external KoaApplication - * @see https://github.com/koajs/koa/blob/master/docs/api/index.md#application - */ +module.exports = Cli diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js new file mode 100644 index 0000000..e3cf187 --- /dev/null +++ b/lib/middleware-stack.js @@ -0,0 +1,211 @@ +'use strict' +const arrayify = require('array-back') +const path = require('path') +const url = require('url') +const debug = require('debug')('local-web-server') +const mw = require('./middleware') + +class MiddlewareStack extends Array { + constructor (options) { + super() + options = Object.assign({ + static: {}, + serveIndex: { + options: { + icons: true, + hidden: true + } + }, + cacheControl: {}, + spa: null, + log: {}, + compress: false, + mime: {}, + forbid: [], + rewrite: [], + verbose: false, + mocks: [] + }, options) + + if (options.verbose) { + process.env.DEBUG = '*' + } + + const log = options.log + log.options = log.options || {} + + if (options.verbose && !log.format) { + log.format = 'none' + } + this.log = log + + if (!options.static.root) options.static.root = process.cwd() + if (!options.serveIndex.path) options.serveIndex.path = process.cwd() + options.rewrite = arrayify(options.rewrite) + options.forbid = arrayify(options.forbid) + options.mocks = arrayify(options.mocks) + this.options = options + } + + /** + * allow from any origin + */ + addCors () { + this.push(require('kcors')()) + return this + } + + /* pretty print JSON */ + addJson () { + this.push(require('koa-json')()) + return this + } + + /* rewrite rules */ + addRewrite () { + const _ = require('koa-route') + + const options = this.options.rewrite + if (options.length) { + options.forEach(route => { + if (route.to) { + /* `to` address is remote if the url specifies a host */ + if (url.parse(route.to).host) { + debug('proxy rewrite', `${route.from} -> ${route.to}`) + this.push(_.all(route.from, mw.proxyRequest(route))) + } else { + const rewrite = require('koa-rewrite') + const rmw = rewrite(route.from, route.to) + rmw._name = 'rewrite' + this.push(rmw) + } + } + }) + } + return this + } + + /* must come after rewrite. + See https://github.com/nodejitsu/node-http-proxy/issues/180. */ + addBodyParser () { + this.push(require('koa-bodyparser')()) + } + + /* path blacklist */ + addBlacklist () { + const options = this.options.forbid + if (options.length) { + debug('forbid', options.join(', ')) + this.push(mw.blacklist(options)) + } + } + + /* cache */ + addCache () { + if (!this.options['no-cache']) { + this.push(require('koa-conditional-get')()) + this.push(require('koa-etag')()) + } + } + + /* mime-type overrides */ + addMimeType () { + const options = this.options.mime + if (options) { + debug('mime override', JSON.stringify(options)) + this.push(mw.mime(options)) + } + } + + /* compress response */ + addCompression () { + if (this.options.compress) { + const compress = require('koa-compress') + debug('compression', 'enabled') + this.push(compress()) + } + } + + /* Logging */ + addLogging () { + const log = this.log + if (log.format !== 'none') { + const morgan = require('koa-morgan') + + if (!log.format) { + const streamLogStats = require('stream-log-stats') + log.options.stream = streamLogStats({ refreshRate: 500 }) + this.push(morgan('common', log.options)) + } else if (log.format === 'logstalgia') { + morgan.token('date', () => { + var d = new Date() + return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '') + }) + this.push(morgan('combined', log.options)) + } else { + this.push(morgan(log.format, log.options)) + } + } + } + + /* Mock Responses */ + addMockResponses () { + const options = this.options.mocks + options.forEach(mock => { + if (mock.module) { + mock.responses = require(path.resolve(path.join(this.options.static.root, mock.module))) + } + + if (mock.responses) { + this.push(mw.mockResponses(mock.route, mock.responses)) + } else if (mock.response) { + mock.target = { + request: mock.request, + response: mock.response + } + this.push(mw.mockResponses(mock.route, mock.target)) + } + }) + } + + /* for any URL not matched by static (e.g. `/search`), serve the SPA */ + addSpa () { + if (this.options.spa) { + const historyApiFallback = require('koa-connect-history-api-fallback') + debug('SPA', this.options.spa) + this.push(historyApiFallback({ + index: this.options.spa, + verbose: this.options.verbose + })) + } + } + + /* serve static files */ + addStatic () { + const options = this.options.static + if (options.root) { + const serve = require('koa-static') + this.push(serve(options.root, options.options)) + } + return this + } + + /* serve directory index */ + addIndex () { + const options = this.options.serveIndex + if (options.path) { + const serveIndex = require('koa-serve-index') + this.push(serveIndex(options.path, options.options)) + } + return this + } + + getMiddleware (options) { + const compose = require('koa-compose') + const convert = require('koa-convert') + const middlewareStack = this.map(convert) + return compose(middlewareStack) + } +} + +module.exports = MiddlewareStack diff --git a/package.json b/package.json index cad8837..8e22610 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,7 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", - "command-line-args": "^3.0.0", - "command-line-usage": "^3.0.1", + "command-line-tool": "75lb/command-line-tool", "config-master": "^2.0.2", "debug": "^2.2.0", "http-proxy": "^1.13.3", From 027c25f53fb154811fc2d4ec05037ff3ed1ff0c6 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 9 Jun 2016 22:54:57 +0100 Subject: [PATCH 008/136] refactor --- bin/cli.js | 28 +++++++++--------- extend/cache-control.js | 28 +++++++----------- extend/live-reload.js | 18 +++++------ lib/local-web-server.js | 29 ++++-------------- lib/middleware-stack.js | 79 +++++++++++++++++++++++++------------------------ 5 files changed, 78 insertions(+), 104 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index c00b81c..d12c404 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -3,18 +3,18 @@ const Cli = require('../') const ws = new Cli() -ws.middleware.addCors() -ws.middleware.addJson() -ws.middleware.addRewrite() -ws.middleware.addBodyParser() -ws.middleware.addBlacklist() -ws.middleware.addCache() -ws.middleware.addMimeType() -ws.middleware.addCompression() -ws.middleware.addLogging() -ws.middleware.addMockResponses() -ws.middleware.addSpa() - -ws.middleware.addStatic() -ws.middleware.addIndex() +ws.middleware + .addCors() + .addJson() + .addRewrite() + .addBodyParser() + .addBlacklist() + .addCache() + .addMimeType() + .addCompression() + .addLogging() + .addMockResponses() + .addSpa() + .addStatic() + .addIndex() ws.listen() diff --git a/extend/cache-control.js b/extend/cache-control.js index d1c56dc..db4e141 100644 --- a/extend/cache-control.js +++ b/extend/cache-control.js @@ -1,24 +1,16 @@ 'use strict' -const Cli = require('../cli') +const Cli = require('../') const cacheControl = require('koa-cache-control') const cliData = require('../lib/cli-data') -cliData.push({ name: 'black' }) - -const ws = new Cli({ - 'no-cache': true, - log: { format: 'dev' } -}) - -ws.middleware.splice( - ws.middleware.findIndex(m => m.name === 'mime-type'), - 1, - { - name: 'cache-control', - create: convert(cacheControl({ - maxAge: 15 - })) - } -) +cliData.optionDefinitions.push({ name: 'maxage', group: 'misc' }) +const ws = new Cli() +ws.middleware + .addLogging('dev') + .add(cacheControl({ + maxAge: 15 + })) + .addStatic() + .addIndex() ws.listen() diff --git a/extend/live-reload.js b/extend/live-reload.js index 6100fb5..90f48f3 100644 --- a/extend/live-reload.js +++ b/extend/live-reload.js @@ -1,14 +1,10 @@ 'use strict' -const Koa = require('koa') -const localWebServer = require('../') +const Cli = require('../') const liveReload = require('koa-livereload') -const convert = require('koa-convert') -const app = new Koa() -const ws = localWebServer({ - log: { format: 'dev' } -}) - -app.use(liveReload()) -app.use(ws) -app.listen(8000) +const ws = new Cli() +ws.middleware + .addLogging('dev') + .add(liveReload()) + .addStatic() +ws.listen(8000) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 8d76b36..4185ca4 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -8,12 +8,12 @@ const Tool = require('command-line-tool') const tool = new Tool() class Cli { - constructor () { + constructor (options) { this.options = null this.app = null this.middleware = null - let options = collectOptions() + options = collectOptions() this.options = options if (options.misc.config) { @@ -24,23 +24,8 @@ class Cli { this.app = app const MiddlewareStack = require('./middleware-stack') - this.middleware = new MiddlewareStack({ - static: { - root: options.server.directory, - options: { - hidden: true - } - }, - serveIndex: { - path: options.server.directory, - options: { - icons: true, - hidden: true - } - }, - log: { - format: options.server['log-format'] - }, + this.middleware = new MiddlewareStack(options) + const a = { compress: options.server.compress, mime: options.server.mime, forbid: options.server.forbid, @@ -49,7 +34,7 @@ class Cli { rewrite: options.server.rewrite, verbose: options.server.verbose, mocks: options.server.mocks - }) + } app.on('error', err => { if (options.server['log-format']) { @@ -121,9 +106,7 @@ function collectOptions () { const builtIn = { port: 8000, - directory: process.cwd(), - forbid: [], - rewrite: [] + directory: process.cwd() } if (options.server.rewrite) { diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index e3cf187..e1a911e 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -8,17 +8,11 @@ const mw = require('./middleware') class MiddlewareStack extends Array { constructor (options) { super() + this.options = options + options = Object.assign({ - static: {}, - serveIndex: { - options: { - icons: true, - hidden: true - } - }, cacheControl: {}, spa: null, - log: {}, compress: false, mime: {}, forbid: [], @@ -31,20 +25,14 @@ class MiddlewareStack extends Array { process.env.DEBUG = '*' } - const log = options.log - log.options = log.options || {} - - if (options.verbose && !log.format) { - log.format = 'none' - } - this.log = log - - if (!options.static.root) options.static.root = process.cwd() - if (!options.serveIndex.path) options.serveIndex.path = process.cwd() options.rewrite = arrayify(options.rewrite) options.forbid = arrayify(options.forbid) options.mocks = arrayify(options.mocks) - this.options = options + } + + add (middleware) { + this.push(middleware) + return this } /** @@ -63,14 +51,13 @@ class MiddlewareStack extends Array { /* rewrite rules */ addRewrite () { - const _ = require('koa-route') - const options = this.options.rewrite if (options.length) { options.forEach(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}`) this.push(_.all(route.from, mw.proxyRequest(route))) } else { @@ -89,6 +76,7 @@ class MiddlewareStack extends Array { See https://github.com/nodejitsu/node-http-proxy/issues/180. */ addBodyParser () { this.push(require('koa-bodyparser')()) + return this } /* path blacklist */ @@ -98,6 +86,7 @@ class MiddlewareStack extends Array { debug('forbid', options.join(', ')) this.push(mw.blacklist(options)) } + return this } /* cache */ @@ -106,6 +95,7 @@ class MiddlewareStack extends Array { this.push(require('koa-conditional-get')()) this.push(require('koa-etag')()) } + return this } /* mime-type overrides */ @@ -115,6 +105,7 @@ class MiddlewareStack extends Array { debug('mime override', JSON.stringify(options)) this.push(mw.mime(options)) } + return this } /* compress response */ @@ -124,28 +115,36 @@ class MiddlewareStack extends Array { debug('compression', 'enabled') this.push(compress()) } + return this } /* Logging */ - addLogging () { - const log = this.log - if (log.format !== 'none') { + addLogging (format, options) { + format = this.options.server['log-format'] || format + options = options || {} + + if (this.options.verbose && !format) { + format = 'none' + } + + if (format !== 'none') { const morgan = require('koa-morgan') - if (!log.format) { + if (!format) { const streamLogStats = require('stream-log-stats') - log.options.stream = streamLogStats({ refreshRate: 500 }) - this.push(morgan('common', log.options)) - } else if (log.format === 'logstalgia') { + options.stream = streamLogStats({ refreshRate: 500 }) + this.push(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)', '') }) - this.push(morgan('combined', log.options)) + this.push(morgan('combined', options)) } else { - this.push(morgan(log.format, log.options)) + this.push(morgan(format, options)) } } + return this } /* Mock Responses */ @@ -166,6 +165,7 @@ class MiddlewareStack extends Array { this.push(mw.mockResponses(mock.route, mock.target)) } }) + return this } /* for any URL not matched by static (e.g. `/search`), serve the SPA */ @@ -178,24 +178,27 @@ class MiddlewareStack extends Array { verbose: this.options.verbose })) } + return this } /* serve static files */ - addStatic () { - const options = this.options.static - if (options.root) { + addStatic (root, options) { + root = this.options.server.directory || root || process.cwd() + options = Object.assign({ hidden: true }, options) + if (root) { const serve = require('koa-static') - this.push(serve(options.root, options.options)) + this.push(serve(root, options)) } return this } /* serve directory index */ - addIndex () { - const options = this.options.serveIndex - if (options.path) { + addIndex (path, options) { + path = this.options.server.directory || path || process.cwd() + options = Object.assign({ icons: true, hidden: true }, options) + if (path) { const serveIndex = require('koa-serve-index') - this.push(serveIndex(options.path, options.options)) + this.push(serveIndex(path, options)) } return this } From 789b82160d9691cb64ace5a65142ec15bef3ebab Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 15 Jun 2016 21:02:52 +0100 Subject: [PATCH 009/136] refactor --- bin/cli.js | 4 +-- extend/cache-control.js | 4 +-- lib/local-web-server.js | 40 ++++++++++--------------- lib/middleware-stack.js | 78 ++++++++++++++++++++++++------------------------- 4 files changed, 57 insertions(+), 69 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index d12c404..49b1b98 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,8 +1,8 @@ #!/usr/bin/env node 'use strict' -const Cli = require('../') +const LocalWebServer = require('../') -const ws = new Cli() +const ws = new LocalWebServer() ws.middleware .addCors() .addJson() diff --git a/extend/cache-control.js b/extend/cache-control.js index db4e141..c25226e 100644 --- a/extend/cache-control.js +++ b/extend/cache-control.js @@ -1,11 +1,11 @@ 'use strict' -const Cli = require('../') +const LocalWebServer = require('../') const cacheControl = require('koa-cache-control') const cliData = require('../lib/cli-data') cliData.optionDefinitions.push({ name: 'maxage', group: 'misc' }) -const ws = new Cli() +const ws = new LocalWebServer() ws.middleware .addLogging('dev') .add(cacheControl({ diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 4185ca4..24eb566 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -8,57 +8,47 @@ const Tool = require('command-line-tool') const tool = new Tool() class Cli { - constructor (options) { - this.options = null + constructor () { + this.options = collectOptions() this.app = null this.middleware = null - options = collectOptions() - this.options = options + const options = this.options if (options.misc.config) { tool.stop(JSON.stringify(options.server, null, ' '), 0) } else { const Koa = require('koa') const app = new Koa() - this.app = app - - const MiddlewareStack = require('./middleware-stack') - this.middleware = new MiddlewareStack(options) - const a = { - compress: options.server.compress, - mime: options.server.mime, - forbid: options.server.forbid, - spa: options.server.spa, - 'no-cache': options.server['no-cache'], - rewrite: options.server.rewrite, - verbose: options.server.verbose, - mocks: options.server.mocks - } - app.on('error', err => { if (options.server['log-format']) { console.error(ansi.format(err.message, 'red')) } }) + this.app = app + + const MiddlewareStack = require('./middleware-stack') + this.middleware = new MiddlewareStack(options) } } listen () { - this.app.use(this.middleware.getMiddleware()) + this.app.use(this.middleware.compose()) const options = this.options + const key = this.options.server.key + const cert = this.options.server.cert if (options.server.https) { - options.server.key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') - options.server.cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') + key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') + cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') } - if (options.server.key && options.server.cert) { + if (key && cert) { const https = require('https') const fs = require('fs') const serverOptions = { - key: fs.readFileSync(options.server.key), - cert: fs.readFileSync(options.server.cert) + key: fs.readFileSync(key), + cert: fs.readFileSync(cert) } const server = https.createServer(serverOptions, this.app.callback()) diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index e1a911e..94f3205 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -4,30 +4,16 @@ const path = require('path') const url = require('url') const debug = require('debug')('local-web-server') const mw = require('./middleware') +const t = require('typical') class MiddlewareStack extends Array { constructor (options) { super() this.options = options - options = Object.assign({ - cacheControl: {}, - spa: null, - compress: false, - mime: {}, - forbid: [], - rewrite: [], - verbose: false, - mocks: [] - }, options) - if (options.verbose) { process.env.DEBUG = '*' } - - options.rewrite = arrayify(options.rewrite) - options.forbid = arrayify(options.forbid) - options.mocks = arrayify(options.mocks) } add (middleware) { @@ -50,8 +36,8 @@ class MiddlewareStack extends Array { } /* rewrite rules */ - addRewrite () { - const options = this.options.rewrite + addRewrite (rewriteRules) { + const options = arrayify(this.options.server.rewrite || rewriteRules) if (options.length) { options.forEach(route => { if (route.to) { @@ -80,18 +66,26 @@ class MiddlewareStack extends Array { } /* path blacklist */ - addBlacklist () { - const options = this.options.forbid - if (options.length) { - debug('forbid', options.join(', ')) - this.push(mw.blacklist(options)) + addBlacklist (forbidList) { + forbidList = arrayify(this.options.server.forbid || forbidList) + if (forbidList.length) { + const pathToRegexp = require('path-to-regexp') + debug('forbid', forbidList.join(', ')) + this.push(function blacklist (ctx, next) { + if (forbidList.some(expression => pathToRegexp(expression).test(ctx.path))) { + ctx.throw(403, http.STATUS_CODES[403]) + } else { + return next() + } + }) } return this } /* cache */ addCache () { - if (!this.options['no-cache']) { + const noCache = this.options.server['no-cache'] + if (!noCache) { this.push(require('koa-conditional-get')()) this.push(require('koa-etag')()) } @@ -99,21 +93,23 @@ class MiddlewareStack extends Array { } /* mime-type overrides */ - addMimeType () { - const options = this.options.mime - if (options) { - debug('mime override', JSON.stringify(options)) - this.push(mw.mime(options)) + addMimeType (mime) { + mime = this.options.server.mime || mime + if (mime) { + debug('mime override', JSON.stringify(mime)) + this.push(mw.mime(mime)) } return this } /* compress response */ - addCompression () { - if (this.options.compress) { - const compress = require('koa-compress') + addCompression (compress) { + compress = t.isDefined(this.options.server.compress) + ? this.options.server.compress + : compress + if (compress) { debug('compression', 'enabled') - this.push(compress()) + this.push(require('koa-compress')()) } return this } @@ -148,10 +144,11 @@ class MiddlewareStack extends Array { } /* Mock Responses */ - addMockResponses () { - const options = this.options.mocks - options.forEach(mock => { + addMockResponses (mocks) { + mocks = arrayify(this.options.server.mocks || mocks) + mocks.forEach(mock => { if (mock.module) { + // TODO: ENSURE this.options.static.root is correct value mock.responses = require(path.resolve(path.join(this.options.static.root, mock.module))) } @@ -169,12 +166,13 @@ class MiddlewareStack extends Array { } /* for any URL not matched by static (e.g. `/search`), serve the SPA */ - addSpa () { - if (this.options.spa) { + addSpa (spa) { + spa = t.isDefined(this.options.server.spa) ? this.options.server.spa : spa + if (spa) { const historyApiFallback = require('koa-connect-history-api-fallback') - debug('SPA', this.options.spa) + debug('SPA', spa) this.push(historyApiFallback({ - index: this.options.spa, + index: spa, verbose: this.options.verbose })) } @@ -203,7 +201,7 @@ class MiddlewareStack extends Array { return this } - getMiddleware (options) { + compose (options) { const compose = require('koa-compose') const convert = require('koa-convert') const middlewareStack = this.map(convert) From 9c3db53d276c1330bc9270395c9b82c49bad530a Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 16 Jun 2016 23:00:07 +0100 Subject: [PATCH 010/136] refactor --- README.md | 2 +- bin/cli.js | 5 +- extend/cache-control.js | 17 +-- extend/index.html | 10 ++ extend/live-reload.js | 7 +- lib/cli-data.js | 97 ++++++--------- lib/local-web-server.js | 99 ++++++++-------- lib/middleware-stack.js | 310 ++++++++++++++++++++++++++++++------------------ lib/middleware.js | 12 -- 9 files changed, 308 insertions(+), 251 deletions(-) create mode 100644 extend/index.html diff --git a/README.md b/README.md index 14e8e6b..24ea340 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.*** # local-web-server -A simple web-server for productive front-end development. Typical use cases: +A simple, extensible web-server for productive front-end development. Typical use cases: * Front-end Development * Static or Single Page App development diff --git a/bin/cli.js b/bin/cli.js index 49b1b98..328ec15 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -3,8 +3,7 @@ const LocalWebServer = require('../') const ws = new LocalWebServer() -ws.middleware - .addCors() +ws.addCors() .addJson() .addRewrite() .addBodyParser() @@ -17,4 +16,4 @@ ws.middleware .addSpa() .addStatic() .addIndex() -ws.listen() + .start() diff --git a/extend/cache-control.js b/extend/cache-control.js index c25226e..bd33057 100644 --- a/extend/cache-control.js +++ b/extend/cache-control.js @@ -1,16 +1,17 @@ 'use strict' const LocalWebServer = require('../') const cacheControl = require('koa-cache-control') -const cliData = require('../lib/cli-data') -cliData.optionDefinitions.push({ name: 'maxage', group: 'misc' }) +const optionDefinitions = { name: 'maxage', type: Number, defaultValue: 1000 } const ws = new LocalWebServer() -ws.middleware - .addLogging('dev') - .add(cacheControl({ - maxAge: 15 - })) +ws.addLogging('dev') + .add({ + optionDefinitions: optionDefinitions, + middleware: function (options) { + return cacheControl({ maxAge: options.middleware.maxage }) + } + }) .addStatic() .addIndex() -ws.listen() + .start() diff --git a/extend/index.html b/extend/index.html new file mode 100644 index 0000000..0cc6591 --- /dev/null +++ b/extend/index.html @@ -0,0 +1,10 @@ + + + + + live-reload demo + + +

Live reloaded attached

+ + diff --git a/extend/live-reload.js b/extend/live-reload.js index 90f48f3..cadb37f 100644 --- a/extend/live-reload.js +++ b/extend/live-reload.js @@ -3,8 +3,7 @@ const Cli = require('../') const liveReload = require('koa-livereload') const ws = new Cli() -ws.middleware - .addLogging('dev') - .add(liveReload()) +ws.addLogging('dev') + .add({ middleware: liveReload }) .addStatic() -ws.listen(8000) + .start() diff --git a/lib/cli-data.js b/lib/cli-data.js index bcd5fd6..bb0ce73 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -4,34 +4,6 @@ exports.optionDefinitions = [ description: 'Web server port.', group: 'server' }, { - name: 'directory', alias: 'd', type: String, typeLabel: '[underline]{path}', - description: 'Root directory, defaults to the current directory.', group: 'server' - }, - { - 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').", group: 'server' - }, - { - 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'.", group: 'server' - }, - { - name: 'spa', alias: 's', type: String, typeLabel: '[underline]{file}', - description: 'Path to a Single Page App, e.g. app.html.', group: 'server' - }, - { - name: 'compress', alias: 'c', type: Boolean, - description: 'Serve gzip-compressed resources, where applicable.', group: 'server' - }, - { - name: 'forbid', alias: 'b', type: String, multiple: true, typeLabel: '[underline]{path} ...', - description: 'A list of forbidden routes.', group: 'server' - }, - { - name: 'no-cache', alias: 'n', type: Boolean, - description: 'Disable etag-based caching - forces loading from disk each request.', group: 'server' - }, - { name: 'key', type: String, typeLabel: '[underline]{file}', group: 'server', description: 'SSL key. Supply along with --cert to launch a https server.' }, @@ -44,43 +16,52 @@ exports.optionDefinitions = [ description: 'Enable HTTPS using a built-in key and cert, registered to the domain 127.0.0.1.' }, { - name: 'verbose', type: Boolean, - description: 'Verbose output, useful for debugging.', group: 'server' - }, - { name: 'help', alias: 'h', type: Boolean, description: 'Print these usage instructions.', group: 'misc' }, { name: 'config', type: Boolean, description: 'Print the stored config.', group: 'misc' - } -] - -exports.usage = [ - { - header: 'local-web-server', - content: 'A simple web-server for productive front-end development.' - }, - { - header: 'Synopsis', - content: [ - '$ ws []', - '$ ws --config', - '$ ws --help' - ] }, { - header: 'Server options', - optionList: exports.optionDefinitions, - group: 'server' - }, - { - header: 'Misc options', - optionList: exports.optionDefinitions, - group: 'misc' - }, - { - content: 'Project home: [underline]{https://github.com/75lb/local-web-server}' + name: 'verbose', type: Boolean, + description: 'Verbose output, useful for debugging.', group: 'misc' } ] + +function usage (middlewareDefinitions) { + return [ + { + header: 'local-web-server', + content: 'A simple web-server for productive front-end development.' + }, + { + header: 'Synopsis', + content: [ + '$ ws []', + '$ ws --config', + '$ ws --help' + ] + }, + { + header: 'Server', + optionList: exports.optionDefinitions, + group: 'server' + }, + { + header: 'Middleware', + optionList: middlewareDefinitions, + group: 'middleware' + }, + { + header: 'Misc', + optionList: exports.optionDefinitions, + group: 'misc' + }, + { + content: 'Project home: [underline]{https://github.com/75lb/local-web-server}' + } + ] +} + +exports.usage = usage diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 24eb566..e54afac 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -5,56 +5,53 @@ const path = require('path') const arrayify = require('array-back') const t = require('typical') const Tool = require('command-line-tool') +const MiddlewareStack = require('./middleware-stack') + const tool = new Tool() -class Cli { - constructor () { - this.options = collectOptions() - this.app = null - this.middleware = null +class Cli extends MiddlewareStack { + start () { + const options = collectOptions(this.getOptionDefinitions()) + this.options = options - const options = this.options + if (options.misc.verbose) { + process.env.DEBUG = '*' + } if (options.misc.config) { - tool.stop(JSON.stringify(options.server, null, ' '), 0) + tool.stop(JSON.stringify(options, null, ' '), 0) } else { const Koa = require('koa') const app = new Koa() app.on('error', err => { - if (options.server['log-format']) { + if (options.middleware['log-format']) { console.error(ansi.format(err.message, 'red')) } }) - this.app = app - const MiddlewareStack = require('./middleware-stack') - this.middleware = new MiddlewareStack(options) - } - } + app.use(this.compose(options)) - listen () { - this.app.use(this.middleware.compose()) - const options = this.options - const key = this.options.server.key - const cert = this.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') - } + 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') + if (key && cert) { + const https = require('https') + const fs = require('fs') - const serverOptions = { - key: fs.readFileSync(key), - cert: fs.readFileSync(cert) - } + const serverOptions = { + key: fs.readFileSync(key), + cert: fs.readFileSync(cert) + } - const server = https.createServer(serverOptions, this.app.callback()) - server.listen(options.server.port, onServerUp.bind(null, options, true)) - } else { - this.app.listen(options.server.port, onServerUp.bind(null, options)) + 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)) + } } } } @@ -65,9 +62,9 @@ function onServerUp (options, isHttps) { .join(', ') console.error(ansi.format( - path.resolve(options.server.directory) === process.cwd() + path.resolve(options.middleware.directory) === process.cwd() ? `serving at ${ipList}` - : `serving [underline]{${options.server.directory}} at ${ipList}` + : `serving [underline]{${options.middleware.directory}} at ${ipList}` )) } @@ -86,36 +83,38 @@ function getIPList (isHttps) { /** * Return default, stored and command-line options combined */ -function collectOptions () { +function collectOptions (mwOptionDefinitions) { const loadConfig = require('config-master') const stored = loadConfig('local-web-server') const cli = require('../lib/cli-data') /* parse command line args */ - let options = tool.getOptions(cli.optionDefinitions, cli.usage) + const definitions = cli.optionDefinitions.concat(arrayify(mwOptionDefinitions)) + let options = tool.getOptions(definitions, cli.usage(definitions)) - const builtIn = { - port: 8000, - directory: process.cwd() - } + /* 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) - if (options.server.rewrite) { - options.server.rewrite = parseRewriteRules(options.server.rewrite) + if (options.middleware.rewrite) { + options.middleware.rewrite = parseRewriteRules(options.middleware.rewrite) } - /* override built-in defaults with stored config and then command line args */ - options.server = Object.assign(builtIn, stored, options.server) - validateOptions(options) + // console.log(options) return options } function parseRewriteRules (rules) { return rules && rules.map(rule => { - const matches = rule.match(/(\S*)\s*->\s*(\S*)/) - return { - from: matches[1], - to: matches[2] + if (t.isString(rule)) { + const matches = rule.match(/(\S*)\s*->\s*(\S*)/) + return { + from: matches[1], + to: matches[2] + } + } else { + return rule } }) } diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index 94f3205..a191d1e 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -5,17 +5,9 @@ const url = require('url') const debug = require('debug')('local-web-server') const mw = require('./middleware') const t = require('typical') +const compose = require('koa-compose') class MiddlewareStack extends Array { - constructor (options) { - super() - this.options = options - - if (options.verbose) { - process.env.DEBUG = '*' - } - } - add (middleware) { this.push(middleware) return this @@ -25,141 +17,196 @@ class MiddlewareStack extends Array { * allow from any origin */ addCors () { - this.push(require('kcors')()) + this.push({ middleware: require('kcors') }) return this } /* pretty print JSON */ addJson () { - this.push(require('koa-json')()) + this.push({ middleware: require('koa-json') }) return this } /* rewrite rules */ addRewrite (rewriteRules) { - const options = arrayify(this.options.server.rewrite || rewriteRules) - if (options.length) { - options.forEach(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}`) - this.push(_.all(route.from, mw.proxyRequest(route))) - } else { - const rewrite = require('koa-rewrite') - const rmw = rewrite(route.from, route.to) - rmw._name = 'rewrite' - this.push(rmw) - } + 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 = arrayify(cliOptions.middleware.rewrite || rewriteRules) + if (options.length) { + options.forEach(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(require('koa-bodyparser')()) + this.push({ middleware: require('koa-bodyparser') }) return this } /* path blacklist */ addBlacklist (forbidList) { - forbidList = arrayify(this.options.server.forbid || forbidList) - if (forbidList.length) { - const pathToRegexp = require('path-to-regexp') - debug('forbid', forbidList.join(', ')) - this.push(function blacklist (ctx, next) { - if (forbidList.some(expression => pathToRegexp(expression).test(ctx.path))) { - ctx.throw(403, http.STATUS_CODES[403]) - } else { - return next() + 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.middleware.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))) { + const http = require('http') + ctx.throw(403, http.STATUS_CODES[403]) + } else { + return next() + } + } } - }) - } + } + }) return this } /* cache */ addCache () { - const noCache = this.options.server['no-cache'] - if (!noCache) { - this.push(require('koa-conditional-get')()) - this.push(require('koa-etag')()) - } + 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.middleware['no-cache'] + if (!noCache) { + return [ + require('koa-conditional-get')(), + require('koa-etag')() + ] + } + } + }) return this } /* mime-type overrides */ addMimeType (mime) { - mime = this.options.server.mime || mime - if (mime) { - debug('mime override', JSON.stringify(mime)) - this.push(mw.mime(mime)) - } + this.push({ + middleware: function (cliOptions) { + mime = cliOptions.middleware.mime || mime + if (mime) { + debug('mime override', JSON.stringify(mime)) + return mw.mime(mime) + } + } + }) return this } /* compress response */ addCompression (compress) { - compress = t.isDefined(this.options.server.compress) - ? this.options.server.compress - : compress - if (compress) { - debug('compression', 'enabled') - this.push(require('koa-compress')()) - } + this.push({ + optionDefinitions: { + name: 'compress', alias: 'c', type: Boolean, + description: 'Serve gzip-compressed resources, where applicable.' + }, + middleware: function (cliOptions) { + compress = t.isDefined(cliOptions.middleware.compress) + ? cliOptions.middleware.compress + : compress + if (compress) { + debug('compression', 'enabled') + return require('koa-compress')() + } + } + }) return this } /* Logging */ addLogging (format, options) { - format = this.options.server['log-format'] || format 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.middleware['log-format'] || format - if (this.options.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 }) - this.push(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)', '') - }) - this.push(morgan('combined', options)) - } else { - this.push(morgan(format, options)) + if (cliOptions.misc.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) { - mocks = arrayify(this.options.server.mocks || mocks) - mocks.forEach(mock => { - if (mock.module) { - // TODO: ENSURE this.options.static.root is correct value - mock.responses = require(path.resolve(path.join(this.options.static.root, mock.module))) - } + this.push({ + middleware: function (cliOptions) { + mocks = arrayify(cliOptions.middleware.mocks || mocks) + mocks.forEach(mock => { + if (mock.module) { + // TODO: ENSURE cliOptions.static.root is correct value + mock.responses = require(path.resolve(path.join(cliOptions.static.root, mock.module))) + } - if (mock.responses) { - this.push(mw.mockResponses(mock.route, mock.responses)) - } else if (mock.response) { - mock.target = { - request: mock.request, - response: mock.response - } - this.push(mw.mockResponses(mock.route, mock.target)) + 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) + } + }) } }) return this @@ -167,44 +214,77 @@ class MiddlewareStack extends Array { /* for any URL not matched by static (e.g. `/search`), serve the SPA */ addSpa (spa) { - spa = t.isDefined(this.options.server.spa) ? this.options.server.spa : spa - if (spa) { - const historyApiFallback = require('koa-connect-history-api-fallback') - debug('SPA', spa) - this.push(historyApiFallback({ - index: spa, - verbose: this.options.verbose - })) - } + 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 = t.isDefined(cliOptions.middleware.spa) ? cliOptions.middleware.spa : spa + if (spa) { + const historyApiFallback = require('koa-connect-history-api-fallback') + debug('SPA', spa) + return historyApiFallback({ + index: spa, + verbose: cliOptions.misc.verbose + }) + } + } + }) return this } /* serve static files */ addStatic (root, options) { - root = this.options.server.directory || root || process.cwd() - options = Object.assign({ hidden: true }, options) - if (root) { - const serve = require('koa-static') - this.push(serve(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) { + root = cliOptions.middleware.directory || root || process.cwd() + options = Object.assign({ hidden: true }, options) + // console.log(root, options, cliOptions) + if (root) { + const serve = require('koa-static') + return serve(root, options) + } + } + }) return this } /* serve directory index */ addIndex (path, options) { - path = this.options.server.directory || path || process.cwd() - options = Object.assign({ icons: true, hidden: true }, options) - if (path) { - const serveIndex = require('koa-serve-index') - this.push(serveIndex(path, options)) - } + this.push({ + middleware: function (cliOptions) { + path = cliOptions.middleware.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 } + getOptionDefinitions () { + const flatten = require('reduce-flatten') + return this + .filter(mw => mw.optionDefinitions) + .map(mw => mw.optionDefinitions) + .reduce(flatten, []) + .map(def => { + def.group = 'middleware' + return def + }) + } compose (options) { - const compose = require('koa-compose') const convert = require('koa-convert') - const middlewareStack = this.map(convert) + const middlewareStack = this + .filter(mw => mw) + .map(mw => compose(arrayify(mw.middleware(options)).map(convert))) return compose(middlewareStack) } } diff --git a/lib/middleware.js b/lib/middleware.js index 650a7a0..620780e 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -1,6 +1,5 @@ 'use strict' const path = require('path') -const http = require('http') const url = require('url') const arrayify = require('array-back') const t = require('typical') @@ -11,7 +10,6 @@ const debug = require('debug')('local-web-server') * @module middleware */ exports.proxyRequest = proxyRequest -exports.blacklist = blacklist exports.mockResponses = mockResponses exports.mime = mime @@ -49,16 +47,6 @@ function proxyRequest (route) { } } -function blacklist (forbid) { - return function blacklist (ctx, next) { - if (forbid.some(expression => pathToRegexp(expression).test(ctx.path))) { - ctx.throw(403, http.STATUS_CODES[403]) - } else { - return next() - } - } -} - function mime (mimeTypes) { return function mime (ctx, next) { return next().then(() => { From aee47bc59963903f4310088edeccd93620d250cf Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 18 Jun 2016 10:13:27 +0100 Subject: [PATCH 011/136] mock names.. debug.. refactor.. options --- example/mock-async/mocks/delayed.js | 1 + example/mock/.local-web-server.json | 2 +- example/mock/mocks/five.js | 1 + example/mock/mocks/stream-self.js | 1 + example/mock/mocks/user.js | 3 + example/spa/.local-web-server.json | 2 +- extend/cache-control.js | 2 +- lib/debug.js | 11 ++++ lib/local-web-server.js | 120 +++++++++++++++++++++--------------- lib/middleware-stack.js | 51 ++++++++------- lib/middleware.js | 6 +- package.json | 1 - 12 files changed, 123 insertions(+), 78 deletions(-) create mode 100644 lib/debug.js 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", From 6298f693cd437148b615e71918d2341d3f4adeef Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 18 Jun 2016 10:29:34 +0100 Subject: [PATCH 012/136] extend examples --- extend/cache-control/.local-web-server.json | 3 +++ extend/{cache-control.js => cache-control/server.js} | 9 +++++---- extend/{ => live-reload-optional}/index.html | 0 extend/live-reload-optional/server.js | 19 +++++++++++++++++++ extend/live-reload/index.html | 10 ++++++++++ extend/{live-reload.js => live-reload/server.js} | 2 +- lib/cli-data.js | 2 +- lib/local-web-server.js | 4 ---- 8 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 extend/cache-control/.local-web-server.json rename extend/{cache-control.js => cache-control/server.js} (61%) rename extend/{ => live-reload-optional}/index.html (100%) create mode 100644 extend/live-reload-optional/server.js create mode 100644 extend/live-reload/index.html rename extend/{live-reload.js => live-reload/server.js} (84%) diff --git a/extend/cache-control/.local-web-server.json b/extend/cache-control/.local-web-server.json new file mode 100644 index 0000000..89cb0a2 --- /dev/null +++ b/extend/cache-control/.local-web-server.json @@ -0,0 +1,3 @@ +{ + "maxage": 2000 +} diff --git a/extend/cache-control.js b/extend/cache-control/server.js similarity index 61% rename from extend/cache-control.js rename to extend/cache-control/server.js index d7fb67e..cf01b8d 100644 --- a/extend/cache-control.js +++ b/extend/cache-control/server.js @@ -1,13 +1,14 @@ 'use strict' -const LocalWebServer = require('../') +const LocalWebServer = require('../../') const cacheControl = require('koa-cache-control') -const optionDefinitions = { name: 'maxage', type: Number, defaultValue: 1000 } - const ws = new LocalWebServer() ws.addLogging('dev') .add({ - optionDefinitions: optionDefinitions, + optionDefinitions: { + name: 'maxage', type: Number, + description: 'The maxage to set on each response.' + }, middleware: function (options) { return cacheControl({ maxAge: options.maxage }) } diff --git a/extend/index.html b/extend/live-reload-optional/index.html similarity index 100% rename from extend/index.html rename to extend/live-reload-optional/index.html diff --git a/extend/live-reload-optional/server.js b/extend/live-reload-optional/server.js new file mode 100644 index 0000000..4c29757 --- /dev/null +++ b/extend/live-reload-optional/server.js @@ -0,0 +1,19 @@ +'use strict' +const Cli = require('../../') +const liveReload = require('koa-livereload') + +const ws = new Cli() +ws.addLogging('dev') + .add({ + optionDefinitions: { + name: 'live-reload', type: Boolean, + description: 'Add live reload.' + }, + middleware: function (options) { + if (options['live-reload']) { + return liveReload() + } + } + }) + .addStatic() + .start() diff --git a/extend/live-reload/index.html b/extend/live-reload/index.html new file mode 100644 index 0000000..0cc6591 --- /dev/null +++ b/extend/live-reload/index.html @@ -0,0 +1,10 @@ + + + + + live-reload demo + + +

Live reloaded attached

+ + diff --git a/extend/live-reload.js b/extend/live-reload/server.js similarity index 84% rename from extend/live-reload.js rename to extend/live-reload/server.js index cadb37f..c79fc28 100644 --- a/extend/live-reload.js +++ b/extend/live-reload/server.js @@ -1,5 +1,5 @@ 'use strict' -const Cli = require('../') +const Cli = require('../../') const liveReload = require('koa-livereload') const ws = new Cli() diff --git a/lib/cli-data.js b/lib/cli-data.js index bb0ce73..3677a53 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -38,7 +38,7 @@ function usage (middlewareDefinitions) { { header: 'Synopsis', content: [ - '$ ws []', + '$ ws [--verbose] [] []', '$ ws --config', '$ ws --help' ] diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 297b98d..0b8f38e 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -146,7 +146,3 @@ function validateOptions (options) { } module.exports = LocalWebServer - -process.on('unhandledRejection', (reason, p) => { - console.error('unhandledRejection', reason, p) -}) From fcabd45058b28db292c102e940051ad155a703bf Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 18 Jun 2016 10:37:52 +0100 Subject: [PATCH 013/136] move rewrite rules --- lib/local-web-server.js | 18 ------------------ lib/middleware-stack.js | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 0b8f38e..2232ad1 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -113,30 +113,12 @@ function collectOptions (mwOptionDefinitions) { directory: process.cwd() }, stored, cliOptions.server, cliOptions.middleware, cliOptions.misc) - if (options.rewrite) { - options.rewrite = parseRewriteRules(options.rewrite) - } // console.error(require('util').inspect(options, { depth: 3, colors: true })) validateOptions(options) return options } -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 - } - }) -} - function validateOptions (options) { if (!t.isNumber(options.port)) { tool.printError('--port must be numeric') diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index 47979c4..4b787b2 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -37,7 +37,7 @@ 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.rewrite || rewriteRules) + const options = parseRewriteRules(arrayify(cliOptions.rewrite || rewriteRules)) if (options.length) { return options.map(route => { if (route.to) { @@ -295,3 +295,18 @@ class MiddlewareStack extends Array { } module.exports = MiddlewareStack + +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 + } + }) +} From f21300380a765bc481571bba3eb7802ce815f02f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 18 Jun 2016 10:43:40 +0100 Subject: [PATCH 014/136] --version option. Fixes #39. --- lib/cli-data.js | 6 +++++- lib/local-web-server.js | 3 +++ lib/middleware.js | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/cli-data.js b/lib/cli-data.js index 3677a53..e258e0c 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -24,8 +24,12 @@ exports.optionDefinitions = [ description: 'Print the stored config.', group: 'misc' }, { - name: 'verbose', type: Boolean, + name: 'verbose', type: Boolean, alias: 'v', description: 'Verbose output, useful for debugging.', group: 'misc' + }, + { + name: 'version', type: Boolean, + description: 'Print the version number.', group: 'misc' } ] diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 2232ad1..7351e95 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -63,6 +63,9 @@ class LocalWebServer extends MiddlewareStack { 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() server.listen(options.port, onServerUp.bind(null, options, server.isHttps)) diff --git a/lib/middleware.js b/lib/middleware.js index 2ee450d..7de477f 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -83,7 +83,7 @@ function mockResponses (route, targets) { target = targets.find(target => !target.request) } - debug(`mock path: ${ctx.path} target: ${target.name || "unnamed"}`) + debug(`mock path: ${ctx.path} target: ${target.name || 'unnamed'}`) if (target) { if (t.isFunction(target.response)) { From 9dcccec17719837773f584a0c60a0faf9beb506b Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 18 Jun 2016 11:39:25 +0100 Subject: [PATCH 015/136] added 'spa-asset-test' config option. Fixes #36 --- example/spa/.local-web-server.json | 3 ++- example/spa/image.jpg | Bin 0 -> 2313 bytes example/spa/index.html | 2 ++ lib/middleware-stack.js | 15 +++++++++------ package.json | 2 -- 5 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 example/spa/image.jpg diff --git a/example/spa/.local-web-server.json b/example/spa/.local-web-server.json index d5db15c..4921b3c 100644 --- a/example/spa/.local-web-server.json +++ b/example/spa/.local-web-server.json @@ -1,3 +1,4 @@ { - "spa": "index.html" + "spa": "index.html", + "no-cache": true } diff --git a/example/spa/image.jpg b/example/spa/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..953ed0f252e9037dfa3d0c64ffa24aeec96320f3 GIT binary patch literal 2313 zcmbV~dpOkF8pqd|8ej0*gVXi9rKjFXK%>Eevdg*WpEVOdv6cl7ru9U`0n{ui!6Y>izgR1 z)g86zSbsWHZ=aj~G85du4JWCb`5<*kgm;04GT?v>SoHNu8W^kw20jFI^c8r_v`v?x z4x@y{;R!@#719hep+X%fVX#;w92SqCx(1Vgo&%f)UUPxvQi9g1twg;&l*P$e7nK<< zW%smwo{AP)1w{O)LZa&E&Z6lX7&48Ft!-@W?3XyWE^~9|czAmG{;+zD-`aKS1GfbQ zhinhsv3Fl&RCG*iT*`rihf)t8Njs5!^3>^^GiP)23oaEF6<@w`^~TNeipr|$TQ&C| zJgjeM6f`|*ZEJt_yyL~oSG|4x1A{|v-wnSPOFoZ%`8qC@O-ym2dj5fh`aj72hf4$H zQo`Y|IN}r+Mk#sL~uYvvef02C$`;+T6a0LYPbMQC<0sSH(L7AjZBB`j5C~9O?bsdVXt`3DtrOnl+ z(`GZ~P^t4x=Q9ipjf{BPK+fV2DH_NtvWYB55&cR2uVd8|*~KQWd@e zs#pv>$>9a=Fla_TGPe-8_c* zm;-@!ykTh8+{sS<^6h6E-?n`zozWKWbah8=P+ehhs9p3LyBT#xecVSQ@0SQc+^z!g zjm`+`PFgHExcZcLqN_n|O#<0Fg%y=`ne(NBC(2X%*;{(2Z~0G0gUW9DDg;jsB%1OH zci-F)%NyBxnmztBN0#R5uohV;y{4O0;?X5k-NrK>=wyHsyPZ7q??#E^{lo#0zn1gH zM>%79)r+qt_J<4$saxV4{dMEL_#-+ivPZDyk{zFIWeGfE8|?D{>9dm338DEvvxga_ z(vHlP`}8ti2S=s%+ZT8{Rnjx6Hpaf97%CnYDAv?P)QD9fVEP-svAjPpkp0f8U17A5 zTofqn-bDy*+2#DvrJ|2RL(%B7lrp)Iw+lvd;cpY850vGnDT6z=L$$4DHVoP`7 z4Tc`C@*#LNXu8U<{+2O(w;It<;nm@s2Ce;D&kRbAZ0j6M?K!TZyY zPM5D+3W3YH%Mfg7s||uc?{o!P;LCeQNMgB4xkoe8STn2m+X~gHq}uBf=&JqcwV|>o3L|lPpN7rue4=fhaqyl-Ot6rSEIgJbmv|h76MIsMzdTJ zymBC{Jdb?Taeo-~Y~Sd9Zn;loWtADl!!tl<$MMCnPCFXwSuA~N8E<$DDRbvtTX11; zk`)u;x-ykUD9`2Qe68sGCHCVJXTfN8Q=l*uX=Y0}@z%xCEkW}02QBAi9Ai2$v3>p- zuE-)z#+{M35bVq^mT#wvBjUp#@Mll%)HbkA*du}fSr$%)pyL320u=N@AU|A3N2O6_ zk6~7D;L{<*)K2b)n9|4O-4nP-dVLB6I$HG)5Pmaq^w_C}gt?O}2%2V#N)>Trg>q|R zGf(0!H$#QvPg)i+o-*@PdAbk?ic8QtPIgaV3dO1NwA^MztPp}c8jybOxzb$EMuq8c zhoE(xF*|vHe+0pG{`6@H1b5dNEx0+_+nU^ZCaERPo7m7z%?YWeYx63iiXoWq<{KZf ziBFNceX`yO!Ev8UA-^X6CcNEiaF~7jt@)tIYTnE1E3JALokN86t%z@RQKiOv!$M-A z1Lke<%gJipTius6Fbb6!0F;iiXR+v7~|KCb@iYAd|{{NQgstX&o4zf(> z(rUzl{zaWVVQl;X(U)6@6ClWlRB*VhGdr&--k#Iq?9r8Z=?$i6WY#2vT3)y{>BMWI ziK-NF5PWE;Ks2R^tH&k+ME>`Rg(WvbqWsXVem@ z^T*m3H6m`-Et%_`n0e$Qh+Q!GNY|p!WVaK8QB)s02nrhP`!BfLmzED!9r95vb03JX zV_KJ3>D(nX^@Nj44h#D3tFLaqLfq1mBCxrG&QRM#Q5s<8DgCFDjr1EnAX8#e8l!W& zj)sTq39_lR9Nk}PcF*}`&vo|U=+eAh?Dh(SL%Vs{T9YeM_T8-En(pJ~^M=Asd(5fH eixO;lF^lmc;PyMDPl2~o%TFRPL4Fb&zWg0P5bm-7 literal 0 HcmV?d00001 diff --git a/example/spa/index.html b/example/spa/index.html index 9a46a23..78bf089 100644 --- a/example/spa/index.html +++ b/example/spa/index.html @@ -7,6 +7,8 @@
  • /login
  • /search
  • +

    Found asset:

    +

    Missing asset (should 404):

    diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index 4b787b2..e1103e5 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -214,20 +214,23 @@ class MiddlewareStack extends Array { } /* for any URL not matched by static (e.g. `/search`), serve the SPA */ - addSpa (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 = t.isDefined(cliOptions.spa) ? cliOptions.spa : spa + spa = cliOptions.spa || spa || 'index.html' + assetTest = new RegExp(cliOptions['spa-asset-test'] || assetTest || '\\.') if (spa) { - const historyApiFallback = require('koa-connect-history-api-fallback') + const send = require('koa-send') + const _ = require('koa-route') debug('SPA', spa) - return historyApiFallback({ - index: spa, - verbose: cliOptions.verbose + return _.get('*', function spaMw (ctx, route, next) { + const root = path.resolve(cliOptions.directory || process.cwd()) + debug(`SPA request. Route: ${route}, isAsset: ${assetTest.test(route)}`) + return send(ctx, assetTest.test(route) ? route : spa, { root: root }).then(next) }) } } diff --git a/package.json b/package.json index a88acf4..4440fc8 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "koa-compose": "^3.1.0", "koa-compress": "^1.0.9", "koa-conditional-get": "^1.0.3", - "koa-connect-history-api-fallback": "~0.3.0", "koa-convert": "^1.2.0", "koa-etag": "^2.1.1", "koa-json": "^1.1.3", @@ -53,7 +52,6 @@ "path-to-regexp": "^1.5.0", "reduce-flatten": "^1.0.0", "stream-log-stats": "^1.1.3", - "string-tools": "^1.0.0", "test-value": "^2.0.0", "typical": "^2.4.2" }, From 343399c1d91caefce0a6290c7ecccaecc781b225 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 18 Jun 2016 21:17:20 +0100 Subject: [PATCH 016/136] update tests.. refactor --- bin/cli.js | 2 +- extend/cache-control/server.js | 2 +- extend/live-reload-optional/server.js | 2 +- extend/live-reload/server.js | 2 +- lib/local-web-server.js | 51 ++++++------- lib/middleware-stack.js | 2 +- package.json | 2 +- test/{fixture/rewrite/one.html => proxy/file.txt} | 0 test/proxy/one.html | 1 + test/proxy/rewrite-proxy.js | 84 +++++++++++++++++++++ test/rewrite-proxy.js | 89 ----------------------- 11 files changed, 114 insertions(+), 123 deletions(-) rename test/{fixture/rewrite/one.html => proxy/file.txt} (100%) create mode 100644 test/proxy/one.html create mode 100644 test/proxy/rewrite-proxy.js delete mode 100644 test/rewrite-proxy.js diff --git a/bin/cli.js b/bin/cli.js index 328ec15..371ba28 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -16,4 +16,4 @@ ws.addCors() .addSpa() .addStatic() .addIndex() - .start() + .listen() diff --git a/extend/cache-control/server.js b/extend/cache-control/server.js index cf01b8d..f72e532 100644 --- a/extend/cache-control/server.js +++ b/extend/cache-control/server.js @@ -15,4 +15,4 @@ ws.addLogging('dev') }) .addStatic() .addIndex() - .start() + .listen() diff --git a/extend/live-reload-optional/server.js b/extend/live-reload-optional/server.js index 4c29757..7c48b99 100644 --- a/extend/live-reload-optional/server.js +++ b/extend/live-reload-optional/server.js @@ -16,4 +16,4 @@ ws.addLogging('dev') } }) .addStatic() - .start() + .listen() diff --git a/extend/live-reload/server.js b/extend/live-reload/server.js index c79fc28..9f2a00e 100644 --- a/extend/live-reload/server.js +++ b/extend/live-reload/server.js @@ -6,4 +6,4 @@ const ws = new Cli() ws.addLogging('dev') .add({ middleware: liveReload }) .addStatic() - .start() + .listen() diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 7351e95..d914464 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -11,19 +11,23 @@ const debug = require('./debug') const tool = new CommandLineTool() class LocalWebServer extends MiddlewareStack { - getApplication () { + _init (options) { + this.options = this.options || Object.assign(options || {}, collectUserOptions(this.getOptionDefinitions())) + } + getApplication (options) { + this._init(options) const Koa = require('koa') const app = new Koa() app.use(this.compose(this.options)) return app } - getServer () { - const options = this.options + getServer (options) { + const app = this.getApplication(options) + 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')) @@ -53,9 +57,9 @@ class LocalWebServer extends MiddlewareStack { return server } - start () { - const options = collectOptions(this.getOptionDefinitions()) - this.options = options + listen (options, callback) { + this._init(options) + options = this.options if (options.verbose) { debug.setLevel(1) @@ -68,21 +72,25 @@ class LocalWebServer extends MiddlewareStack { tool.stop(pkg.version) } else { const server = this.getServer() - server.listen(options.port, onServerUp.bind(null, options, server.isHttps)) + const port = options.port || 8000 + server.listen(port, () => { + onServerUp(port, options.directory, server.isHttps) + if (callback) callback() + }) return server } } } -function onServerUp (options, isHttps) { +function onServerUp (port, directory, isHttps) { const ipList = getIPList(isHttps) - .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) + .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${port}}`) .join(', ') console.error(ansi.format( - path.resolve(options.directory) === process.cwd() + path.resolve(directory || '') === process.cwd() ? `serving at ${ipList}` - : `serving [underline]{${options.directory}} at ${ipList}` + : `serving [underline]{${directory}} at ${ipList}` )) } @@ -101,7 +109,7 @@ function getIPList (isHttps) { /** * Return default, stored and command-line options combined */ -function collectOptions (mwOptionDefinitions) { +function collectUserOptions (mwOptionDefinitions) { const loadConfig = require('config-master') const stored = loadConfig('local-web-server') const cli = require('../lib/cli-data') @@ -110,24 +118,11 @@ function collectOptions (mwOptionDefinitions) { const definitions = cli.optionDefinitions.concat(arrayify(mwOptionDefinitions)) let cliOptions = tool.getOptions(definitions, cli.usage(definitions)) - /* 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) + /* 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 })) - - validateOptions(options) return options } -function validateOptions (options) { - if (!t.isNumber(options.port)) { - tool.printError('--port must be numeric') - console.error(tool.usage) - tool.halt() - } -} - module.exports = LocalWebServer diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index e1103e5..e918f41 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -246,9 +246,9 @@ class MiddlewareStack extends Array { 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) - // console.log(root, options, cliOptions) if (cliOptions.directory) { const serve = require('koa-static') return serve(cliOptions.directory, options) diff --git a/package.json b/package.json index 4440fc8..87da531 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", - "command-line-tool": "75lb/command-line-tool", + "command-line-tool": "~0.3.0", "config-master": "^2.0.2", "http-proxy": "^1.13.3", "kcors": "^1.2.1", diff --git a/test/fixture/rewrite/one.html b/test/proxy/file.txt similarity index 100% rename from test/fixture/rewrite/one.html rename to test/proxy/file.txt diff --git a/test/proxy/one.html b/test/proxy/one.html new file mode 100644 index 0000000..5626abf --- /dev/null +++ b/test/proxy/one.html @@ -0,0 +1 @@ +one diff --git a/test/proxy/rewrite-proxy.js b/test/proxy/rewrite-proxy.js new file mode 100644 index 0000000..0085174 --- /dev/null +++ b/test/proxy/rewrite-proxy.js @@ -0,0 +1,84 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const http = require('http') + +function checkResponse (t, status, body) { + return function (response) { + if (status) t.strictEqual(response.res.statusCode, status) + if (body) t.ok(body.test(response.data), 'correct data') + } +} + +function fail (t) { + return function (err) { + t.fail('failed: ' + err.stack) + } +} + +test('rewrite: proxy', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addRewrite([ + { from: '/test/*', to: 'http://registry.npmjs.org/$1' } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test/') + .then(checkResponse(t, 200, /db_name/)) + .then(server.close.bind(server)) + .catch(fail(t)) + }) +}) + +test('rewrite: proxy, POST', function (t) { + t.plan(1) + const ws = new LocalWebServer() + ws.addRewrite([ + { from: '/test/*', to: 'http://registry.npmjs.org/' } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test/', { data: {} }) + .then(checkResponse(t, 405)) + .then(server.close.bind(server)) + .catch(fail(t)) + }) +}) + +test('rewrite: proxy, two url tokens', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addRewrite([ + { from: '/:package/:version', to: 'http://registry.npmjs.org/:package/:version' } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/command-line-args/1.0.0') + .then(checkResponse(t, 200, /command-line-args/)) + .then(server.close.bind(server)) + .catch(fail(t)) + }) +}) + +test('rewrite: proxy with port', function (t) { + t.plan(2) + const ws1 = new LocalWebServer() + ws1.addStatic(__dirname) + + const ws2 = new LocalWebServer() + ws2.addRewrite([ + { from: '/test/*', to: 'http://localhost:9000/$1' } + ]) + const server1 = ws1.getServer() + const server2 = ws2.getServer() + server1.listen(9000, () => { + server2.listen(8100, () => { + request('http://localhost:8100/test/file.txt') + .then(checkResponse(t, 200, /one/)) + .then(server1.close.bind(server1)) + .then(server2.close.bind(server2)) + }) + }) +}) diff --git a/test/rewrite-proxy.js b/test/rewrite-proxy.js deleted file mode 100644 index 3e884e2..0000000 --- a/test/rewrite-proxy.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const localWebServer = require('../') -const http = require('http') - -function launchServer (app, options) { - options = options || {} - const path = `http://localhost:8100${options.path || '/'}` - const server = http.createServer(app.callback()) - return server.listen(options.port || 8100, () => { - const req = request(path, options.reqOptions) - if (options.onSuccess) req.then(options.onSuccess) - if (!options.leaveOpen) req.then(() => server.close()) - req.catch(err => console.error('LAUNCH ERROR', err.stack)) - }) -} - -function checkResponse (t, status, body) { - return function (response) { - if (status) t.strictEqual(response.res.statusCode, status) - if (body) t.ok(body.test(response.data)) - } -} - -test('rewrite: proxy', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture/rewrite' }, - rewrite: [ { from: '/test/*', to: 'http://registry.npmjs.org/$1' } ] - }) - launchServer(app, { path: '/test/', onSuccess: response => { - t.strictEqual(response.res.statusCode, 200) - t.ok(/db_name/.test(response.data)) - }}) -}) - -test('rewrite: proxy, POST', function (t) { - t.plan(1) - const app = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture/rewrite' }, - rewrite: [ { from: '/test/*', to: 'http://registry.npmjs.org/' } ] - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - request('http://localhost:8100/test/', { data: {} }) - .then(checkResponse(t, 405)) - .then(server.close.bind(server)) - }) -}) - -test('rewrite: proxy, two url tokens', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - rewrite: [ { from: '/:package/:version', to: 'http://registry.npmjs.org/:package/:version' } ] - }) - launchServer(app, { path: '/command-line-args/1.0.0', onSuccess: response => { - t.strictEqual(response.res.statusCode, 200) - t.ok(/command-line-args/.test(response.data)) - }}) -}) - -test('rewrite: proxy with port', function (t) { - t.plan(2) - const one = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture/one' } - }) - const two = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture/spa' }, - rewrite: [ { from: '/test/*', to: 'http://localhost:9000/$1' } ] - }) - const server1 = http.createServer(one.callback()) - const server2 = http.createServer(two.callback()) - server1.listen(9000, () => { - server2.listen(8100, () => { - request('http://localhost:8100/test/file.txt').then(response => { - t.strictEqual(response.res.statusCode, 200) - t.ok(/one/.test(response.data)) - server1.close() - server2.close() - }) - }) - }) -}) From 71b2f4dcb4a988dee0e822107864f6d9a8e7dd2c Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Jun 2016 01:02:51 +0100 Subject: [PATCH 017/136] update tests --- bin/cli.js | 2 +- lib/local-web-server.js | 2 +- lib/middleware-stack.js | 13 +- lib/middleware.js | 2 +- package.json | 2 +- test/common.js | 22 +++ test/{fixture => compress}/big-file.txt | 0 test/compress/compress.js | 22 +++ test/forbid/forbid.js | 21 +++ test/{fixture => }/forbid/one.html | 0 test/{fixture => }/forbid/two.php | 0 test/log/log.js | 25 +++ test/mime/mime.js | 22 +++ test/{fixture => mime}/something.php | 0 test/mock/mock.js | 150 ++++++++++++++++ test/mock/one.html | 1 + test/proxy/rewrite-proxy.js | 29 +-- test/rewrite/one.html | 1 + test/rewrite/rewrite.js | 19 ++ test/serve-index/serve-index.js | 18 ++ test/spa/one.txt | 1 + test/spa/spa.js | 28 +++ test/spa/two.txt | 1 + test/static.js | 33 ---- test/static/file.txt | 1 + test/static/static.js | 18 ++ test/test.js | 305 -------------------------------- 27 files changed, 371 insertions(+), 367 deletions(-) create mode 100644 test/common.js rename test/{fixture => compress}/big-file.txt (100%) create mode 100644 test/compress/compress.js create mode 100644 test/forbid/forbid.js rename test/{fixture => }/forbid/one.html (100%) rename test/{fixture => }/forbid/two.php (100%) create mode 100644 test/log/log.js create mode 100644 test/mime/mime.js rename test/{fixture => mime}/something.php (100%) create mode 100644 test/mock/mock.js create mode 100644 test/mock/one.html create mode 100644 test/rewrite/one.html create mode 100644 test/rewrite/rewrite.js create mode 100644 test/serve-index/serve-index.js create mode 100644 test/spa/one.txt create mode 100644 test/spa/spa.js create mode 100644 test/spa/two.txt delete mode 100644 test/static.js create mode 100644 test/static/file.txt create mode 100644 test/static/static.js delete mode 100644 test/test.js diff --git a/bin/cli.js b/bin/cli.js index 371ba28..d4fbb15 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -9,7 +9,7 @@ ws.addCors() .addBodyParser() .addBlacklist() .addCache() - .addMimeType() + .addMimeOverride() .addCompression() .addLogging() .addMockResponses() diff --git a/lib/local-web-server.js b/lib/local-web-server.js index d914464..2852c2e 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -30,7 +30,7 @@ class LocalWebServer extends MiddlewareStack { app.on('error', err => { if (options['log-format']) { - console.error(ansi.format(err.message, 'red')) + console.error(ansi.format(err.stack, 'red')) } }) diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index e918f41..fda6a26 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -82,8 +82,7 @@ class MiddlewareStack extends Array { debug('forbid', forbidList.join(', ')) return function blacklist (ctx, next) { if (forbidList.some(expression => pathToRegexp(expression).test(ctx.path))) { - const http = require('http') - ctx.throw(403, http.STATUS_CODES[403]) + ctx.status = 403 } else { return next() } @@ -115,7 +114,7 @@ class MiddlewareStack extends Array { } /* mime-type overrides */ - addMimeType (mime) { + addMimeOverride (mime) { this.push({ middleware: function (cliOptions) { mime = cliOptions.mime || mime @@ -229,8 +228,12 @@ class MiddlewareStack extends Array { debug('SPA', spa) return _.get('*', function spaMw (ctx, route, next) { const root = path.resolve(cliOptions.directory || process.cwd()) - debug(`SPA request. Route: ${route}, isAsset: ${assetTest.test(route)}`) - return send(ctx, assetTest.test(route) ? route : spa, { root: root }).then(next) + 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) + } }) } } diff --git a/lib/middleware.js b/lib/middleware.js index 7de477f..964b2b2 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -83,7 +83,7 @@ function mockResponses (route, targets) { target = targets.find(target => !target.request) } - debug(`mock path: ${ctx.path} target: ${target.name || 'unnamed'}`) + debug(`mock path: ${ctx.path} target: ${target && target.name || 'unnamed'}`) if (target) { if (t.isFunction(target.response)) { diff --git a/package.json b/package.json index 87da531..f348e49 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "node": ">=4.0.0" }, "scripts": { - "test": "tape test/*.js", + "test": "tape test/*/*.js", "docs": "jsdoc2md -t jsdoc2md/README.hbs -p list lib/*.js > README.md; echo", "cover": "istanbul cover ./node_modules/.bin/tape test/*.js && cat coverage/lcov.info | coveralls && rm -rf coverage; echo" }, diff --git a/test/common.js b/test/common.js new file mode 100644 index 0000000..43d4ff1 --- /dev/null +++ b/test/common.js @@ -0,0 +1,22 @@ +'use strict' +const arrayify = require('array-back') + +exports.checkResponse = checkResponse +exports.fail = fail + +function checkResponse (t, status, bodyTests) { + return function (response) { + if (status) t.strictEqual(response.res.statusCode, status) + if (bodyTests) { + arrayify(bodyTests).forEach(body => { + t.ok(body.test(response.data), 'correct data') + }) + } + } +} + +function fail (t) { + return function (err) { + t.fail('failed: ' + err.stack) + } +} diff --git a/test/fixture/big-file.txt b/test/compress/big-file.txt similarity index 100% rename from test/fixture/big-file.txt rename to test/compress/big-file.txt diff --git a/test/compress/compress.js b/test/compress/compress.js new file mode 100644 index 0000000..84f057b --- /dev/null +++ b/test/compress/compress.js @@ -0,0 +1,22 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('compress', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addCompression(true) + ws.addStatic(__dirname) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/big-file.txt', { headers: { 'Accept-Encoding': 'gzip' } }) + .then(response => { + t.strictEqual(response.res.statusCode, 200) + t.strictEqual(response.res.headers['content-encoding'], 'gzip') + }) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/forbid/forbid.js b/test/forbid/forbid.js new file mode 100644 index 0000000..c2d4910 --- /dev/null +++ b/test/forbid/forbid.js @@ -0,0 +1,21 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('forbid', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addBlacklist([ '*.php', '*.html' ]) + ws.addStatic(__dirname) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/two.php') + .then(c.checkResponse(t, 403)) + .then(() => request('http://localhost:8100/one.html')) + .then(c.checkResponse(t, 403)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/fixture/forbid/one.html b/test/forbid/one.html similarity index 100% rename from test/fixture/forbid/one.html rename to test/forbid/one.html diff --git a/test/fixture/forbid/two.php b/test/forbid/two.php similarity index 100% rename from test/fixture/forbid/two.php rename to test/forbid/two.php diff --git a/test/log/log.js b/test/log/log.js new file mode 100644 index 0000000..a90e2f6 --- /dev/null +++ b/test/log/log.js @@ -0,0 +1,25 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('logging', function (t) { + t.plan(2) + const ws = new LocalWebServer() + + const stream = require('stream').PassThrough() + stream.on('readable', () => { + let chunk = stream.read() + if (chunk) t.ok(/GET/.test(chunk.toString())) + }) + + ws.addLogging('common', { stream: stream }) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/') + .then(c.checkResponse(t, 404)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/mime/mime.js b/test/mime/mime.js new file mode 100644 index 0000000..bb8cc44 --- /dev/null +++ b/test/mime/mime.js @@ -0,0 +1,22 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('mime override', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addMimeOverride({ 'text/plain': [ 'php' ] }) + ws.addStatic(__dirname) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/something.php') + .then(response => { + t.strictEqual(response.res.statusCode, 200) + t.ok(/text\/plain/.test(response.res.headers['content-type'])) + }) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/fixture/something.php b/test/mime/something.php similarity index 100% rename from test/fixture/something.php rename to test/mime/something.php diff --git a/test/mock/mock.js b/test/mock/mock.js new file mode 100644 index 0000000..28dccac --- /dev/null +++ b/test/mock/mock.js @@ -0,0 +1,150 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('mock: simple response', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addMockResponses([ + { route: '/test', response: { body: 'test' } } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test') + .then(c.checkResponse(t, 200, /test/)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) + +test('mock: method request filter', function (t) { + t.plan(3) + const ws = new LocalWebServer() + ws.addMockResponses([ + { + route: '/test', + request: { method: 'POST' }, + response: { body: 'test' } + } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test') + .then(c.checkResponse(t, 404)) + .then(() => request('http://localhost:8100/test', { data: 'something' })) + .then(c.checkResponse(t, 200, /test/)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) + +test('mock: accepts request filter', function (t) { + t.plan(3) + const ws = new LocalWebServer() + ws.addMockResponses([ + { + route: '/test', + request: { accepts: 'text' }, + response: { body: 'test' } + } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test', { headers: { Accept: '*/json' } }) + .then(c.checkResponse(t, 404)) + .then(() => request('http://localhost:8100/test', { headers: { Accept: 'text/plain' } })) + .then(c.checkResponse(t, 200, /test/)) + .then(server.close.bind(server)) + }) +}) + +test('mock: responses array', function (t) { + t.plan(4) + const ws = new LocalWebServer() + ws.addMockResponses([ + { + route: '/test', + responses: [ + { request: { method: 'GET' }, response: { body: 'get' } }, + { request: { method: 'POST' }, response: { body: 'post' } } + ] + } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test') + .then(c.checkResponse(t, 200, /get/)) + .then(() => request('http://localhost:8100/test', { method: 'POST' })) + .then(c.checkResponse(t, 200, /post/)) + .then(server.close.bind(server)) + }) +}) + +test('mock: response function', function (t) { + t.plan(4) + const ws = new LocalWebServer() + ws.addMockResponses([ + { + route: '/test', + responses: [ + { request: { method: 'GET' }, response: ctx => ctx.body = 'get' }, + { request: { method: 'POST' }, response: ctx => ctx.body = 'post' } + ] + } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test') + .then(c.checkResponse(t, 200, /get/)) + .then(() => request('http://localhost:8100/test', { method: 'POST' })) + .then(c.checkResponse(t, 200, /post/)) + .then(server.close.bind(server)) + }) +}) + +test('mock: response function args', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addMockResponses([ + { + route: '/test/:one', + responses: [ + { request: { method: 'GET' }, response: (ctx, one) => ctx.body = one } + ] + } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test/yeah') + .then(c.checkResponse(t, 200, /yeah/)) + .then(server.close.bind(server)) + }) +}) + +test('mock: async response function', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addMockResponses([ + { + route: '/test', + responses: { + response: function (ctx) { + return new Promise((resolve, reject) => { + setTimeout(() => { + ctx.body = 'test' + resolve() + }, 10) + }) + } + } + } + ]) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/test') + .then(c.checkResponse(t, 200, /test/)) + .then(server.close.bind(server)) + }) +}) diff --git a/test/mock/one.html b/test/mock/one.html new file mode 100644 index 0000000..5626abf --- /dev/null +++ b/test/mock/one.html @@ -0,0 +1 @@ +one diff --git a/test/proxy/rewrite-proxy.js b/test/proxy/rewrite-proxy.js index 0085174..3fea168 100644 --- a/test/proxy/rewrite-proxy.js +++ b/test/proxy/rewrite-proxy.js @@ -3,19 +3,7 @@ const test = require('tape') const request = require('req-then') const LocalWebServer = require('../../') const http = require('http') - -function checkResponse (t, status, body) { - return function (response) { - if (status) t.strictEqual(response.res.statusCode, status) - if (body) t.ok(body.test(response.data), 'correct data') - } -} - -function fail (t) { - return function (err) { - t.fail('failed: ' + err.stack) - } -} +const c = require('../common') test('rewrite: proxy', function (t) { t.plan(2) @@ -26,9 +14,9 @@ test('rewrite: proxy', function (t) { const server = ws.getServer() server.listen(8100, () => { request('http://localhost:8100/test/') - .then(checkResponse(t, 200, /db_name/)) + .then(c.checkResponse(t, 200, /db_name/)) .then(server.close.bind(server)) - .catch(fail(t)) + .catch(c.fail(t)) }) }) @@ -41,9 +29,9 @@ test('rewrite: proxy, POST', function (t) { const server = ws.getServer() server.listen(8100, () => { request('http://localhost:8100/test/', { data: {} }) - .then(checkResponse(t, 405)) + .then(c.checkResponse(t, 405)) .then(server.close.bind(server)) - .catch(fail(t)) + .catch(c.fail(t)) }) }) @@ -56,9 +44,9 @@ test('rewrite: proxy, two url tokens', function (t) { const server = ws.getServer() server.listen(8100, () => { request('http://localhost:8100/command-line-args/1.0.0') - .then(checkResponse(t, 200, /command-line-args/)) + .then(c.checkResponse(t, 200, /command-line-args/)) .then(server.close.bind(server)) - .catch(fail(t)) + .catch(c.fail(t)) }) }) @@ -76,9 +64,10 @@ test('rewrite: proxy with port', function (t) { server1.listen(9000, () => { server2.listen(8100, () => { request('http://localhost:8100/test/file.txt') - .then(checkResponse(t, 200, /one/)) + .then(c.checkResponse(t, 200, /one/)) .then(server1.close.bind(server1)) .then(server2.close.bind(server2)) + .catch(c.fail(t)) }) }) }) diff --git a/test/rewrite/one.html b/test/rewrite/one.html new file mode 100644 index 0000000..5626abf --- /dev/null +++ b/test/rewrite/one.html @@ -0,0 +1 @@ +one diff --git a/test/rewrite/rewrite.js b/test/rewrite/rewrite.js new file mode 100644 index 0000000..cdce7e0 --- /dev/null +++ b/test/rewrite/rewrite.js @@ -0,0 +1,19 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('rewrite local', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addRewrite([ { from: '/two.html', to: '/one.html' } ]) + ws.addStatic(__dirname) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/two.html') + .then(c.checkResponse(t, 200, /one/)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/serve-index/serve-index.js b/test/serve-index/serve-index.js new file mode 100644 index 0000000..3ca09d6 --- /dev/null +++ b/test/serve-index/serve-index.js @@ -0,0 +1,18 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('static', function (t) { + t.plan(3) + const ws = new LocalWebServer() + ws.addIndex(__dirname, { icons: true }) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/') + .then(c.checkResponse(t, 200, [ /listing directory/, /class="icon/ ])) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/spa/one.txt b/test/spa/one.txt new file mode 100644 index 0000000..5626abf --- /dev/null +++ b/test/spa/one.txt @@ -0,0 +1 @@ +one diff --git a/test/spa/spa.js b/test/spa/spa.js new file mode 100644 index 0000000..38dbc50 --- /dev/null +++ b/test/spa/spa.js @@ -0,0 +1,28 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('spa', function (t) { + t.plan(6) + const ws = new LocalWebServer() + ws.addSpa('one.txt') + ws.addStatic(__dirname) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/asdf', { headers: { accept: 'text/html' } }) + .then(c.checkResponse(t, 200, /one/)) + /* html requests for missing files with extensions do not redirect to spa */ + .then(() => request('http://localhost:8100/asdf.txt', { headers: { accept: 'text/html' } })) + .then(c.checkResponse(t, 404)) + /* existing static file */ + .then(() => request('http://localhost:8100/two.txt')) + .then(c.checkResponse(t, 200, /two/)) + /* not a text/html request - does not redirect to spa */ + .then(() => request('http://localhost:8100/asdf', { headers: { accept: 'application/json' } })) + .then(c.checkResponse(t, 404)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/spa/two.txt b/test/spa/two.txt new file mode 100644 index 0000000..f719efd --- /dev/null +++ b/test/spa/two.txt @@ -0,0 +1 @@ +two diff --git a/test/static.js b/test/static.js deleted file mode 100644 index 837d684..0000000 --- a/test/static.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const localWebServer = require('../') -const http = require('http') - -function launchServer (app, options) { - options = options || {} - const path = `http://localhost:8100${options.path || '/'}` - const server = http.createServer(app.callback()) - return server.listen(options.port || 8100, () => { - const req = request(path, options.reqOptions) - if (options.onSuccess) req.then(options.onSuccess) - if (!options.leaveOpen) req.then(() => server.close()) - req.catch(err => console.error('LAUNCH ERROR', err.stack)) - }) -} - -test('static', function (t) { - t.plan(1) - const app = localWebServer({ - log: { format: 'none' }, - static: { - root: __dirname + '/fixture', - options: { - index: 'file.txt' - } - } - }) - launchServer(app, { onSuccess: response => { - t.ok(/test/.test(response.data)) - }}) -}) diff --git a/test/static/file.txt b/test/static/file.txt new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/test/static/file.txt @@ -0,0 +1 @@ +test diff --git a/test/static/static.js b/test/static/static.js new file mode 100644 index 0000000..92cf1f8 --- /dev/null +++ b/test/static/static.js @@ -0,0 +1,18 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../../') +const c = require('../common') + +test('static', function (t) { + t.plan(2) + const ws = new LocalWebServer() + ws.addStatic(__dirname, { index: 'file.txt' }) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/') + .then(c.checkResponse(t, 200, /test/)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) diff --git a/test/test.js b/test/test.js deleted file mode 100644 index 838605d..0000000 --- a/test/test.js +++ /dev/null @@ -1,305 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const localWebServer = require('../') -const http = require('http') -const PassThrough = require('stream').PassThrough - -function launchServer (app, options) { - options = options || {} - const path = `http://localhost:8100${options.path || '/'}` - const server = http.createServer(app.callback()) - return server.listen(options.port || 8100, () => { - const req = request(path, options.reqOptions) - if (options.onSuccess) req.then(options.onSuccess) - if (!options.leaveOpen) req.then(() => server.close()) - req.catch(err => console.error('LAUNCH ERROR', err.stack)) - }) -} - -function checkResponse (t, status, body) { - return function (response) { - if (status) t.strictEqual(response.res.statusCode, status) - if (body) t.ok(body.test(response.data)) - } -} - -test('serve-index', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - serveIndex: { - path: __dirname + '/fixture', - options: { - icons: true - } - } - }) - launchServer(app, { onSuccess: response => { - t.ok(/listing directory/.test(response.data)) - t.ok(/class="icon/.test(response.data)) - }}) -}) - -test('single page app', function (t) { - t.plan(6) - const app = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture/spa' }, - spa: 'one.txt' - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - /* text/html requests for missing files redirect to spa */ - request('http://localhost:8100/asdf', { headers: { accept: 'text/html' } }) - .then(checkResponse(t, 200, /one/)) - /* html requests for missing files with extensions do not redirect to spa */ - .then(() => request('http://localhost:8100/asdf.txt', { headers: { accept: 'text/html' } })) - .then(checkResponse(t, 404)) - /* existing static file */ - .then(() => request('http://localhost:8100/two.txt')) - .then(checkResponse(t, 200, /two/)) - /* not a text/html request - does not redirect to spa */ - .then(() => request('http://localhost:8100/asdf')) - .then(checkResponse(t, 404)) - .then(server.close.bind(server)) - }) -}) - -test('log: common', function (t) { - t.plan(1) - const stream = PassThrough() - - stream.on('readable', () => { - let chunk = stream.read() - if (chunk) t.ok(/GET/.test(chunk.toString())) - }) - - const app = localWebServer({ - log: { - format: 'common', - options: { - stream: stream - } - } - }) - launchServer(app) -}) - -test('compress', function (t) { - t.plan(1) - const app = localWebServer({ - compress: true, - log: { format: 'none' }, - static: { root: __dirname + '/fixture' } - }) - launchServer( - app, - { - reqOptions: { headers: { 'Accept-Encoding': 'gzip' } }, - path: '/big-file.txt', - onSuccess: response => { - t.strictEqual(response.res.headers['content-encoding'], 'gzip') - } - } - ) -}) - -test('mime', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture' }, - mime: { 'text/plain': [ 'php' ] } - }) - launchServer(app, { path: '/something.php', onSuccess: response => { - t.strictEqual(response.res.statusCode, 200) - t.ok(/text\/plain/.test(response.res.headers['content-type'])) - }}) -}) - -test('forbid', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture/forbid' }, - forbid: [ '*.php', '*.html' ] - }) - const server = launchServer(app, { leaveOpen: true }) - request('http://localhost:8100/two.php') - .then(response => { - t.strictEqual(response.res.statusCode, 403) - request('http://localhost:8100/one.html') - .then(response => { - t.strictEqual(response.res.statusCode, 403) - server.close() - }) - }) -}) - -test('rewrite: local', function (t) { - t.plan(1) - const app = localWebServer({ - log: { format: 'none' }, - static: { root: __dirname + '/fixture/rewrite' }, - rewrite: [ { from: '/two.html', to: '/one.html' } ] - }) - launchServer(app, { path: '/two.html', onSuccess: response => { - t.ok(/one/.test(response.data)) - }}) -}) - -test('mock: simple response', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - mocks: [ - { route: '/test', response: { body: 'test' } } - ] - }) - launchServer(app, { path: '/test', onSuccess: response => { - t.strictEqual(response.res.statusCode, 200) - t.ok(/test/.test(response.data)) - }}) -}) - -test('mock: method request filter', function (t) { - t.plan(3) - const app = localWebServer({ - log: { format: 'none' }, - mocks: [ - { - route: '/test', - request: { method: 'POST' }, - response: { body: 'test' } - } - ] - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(checkResponse(t, 404)) - .then(() => request('http://localhost:8100/test', { data: 'something' })) - .then(checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: accepts request filter', function (t) { - t.plan(3) - const app = localWebServer({ - log: { format: 'none' }, - mocks: [ - { - route: '/test', - request: { accepts: 'text' }, - response: { body: 'test' } - } - ] - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - request('http://localhost:8100/test', { headers: { Accept: '*/json' } }) - .then(checkResponse(t, 404)) - .then(() => request('http://localhost:8100/test', { headers: { Accept: 'text/plain' } })) - .then(checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: responses array', function (t) { - t.plan(4) - const app = localWebServer({ - log: { format: 'none' }, - mocks: [ - { - route: '/test', - responses: [ - { request: { method: 'GET' }, response: { body: 'get' } }, - { request: { method: 'POST' }, response: { body: 'post' } } - ] - } - ] - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(checkResponse(t, 200, /get/)) - .then(() => request('http://localhost:8100/test', { method: 'POST' })) - .then(checkResponse(t, 200, /post/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: response function', function (t) { - t.plan(4) - const app = localWebServer({ - log: { format: 'none' }, - mocks: [ - { - route: '/test', - responses: [ - { request: { method: 'GET' }, response: ctx => ctx.body = 'get' }, - { request: { method: 'POST' }, response: ctx => ctx.body = 'post' } - ] - } - ] - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(checkResponse(t, 200, /get/)) - .then(() => request('http://localhost:8100/test', { method: 'POST' })) - .then(checkResponse(t, 200, /post/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: response function args', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - mocks: [ - { - route: '/test/:one', - responses: [ - { request: { method: 'GET' }, response: (ctx, one) => ctx.body = one } - ] - } - ] - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - request('http://localhost:8100/test/yeah') - .then(checkResponse(t, 200, /yeah/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: async response function', function (t) { - t.plan(2) - const app = localWebServer({ - log: { format: 'none' }, - mocks: [ - { - route: '/test', - responses: { - response: function (ctx) { - return new Promise((resolve, reject) => { - setTimeout(() => { - ctx.body = 'test' - resolve() - }, 10) - }) - } - } - } - ] - }) - const server = http.createServer(app.callback()) - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - }) -}) From ef9bbf80662924e8b67dbb8dc24e2e5b35eb07f7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Jun 2016 01:08:21 +0100 Subject: [PATCH 018/136] strict mode --- lib/debug.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/debug.js b/lib/debug.js index 55eca0f..a3122b9 100644 --- a/lib/debug.js +++ b/lib/debug.js @@ -1,3 +1,4 @@ +'use strict' module.exports = debug let level = 0 From 22612bbcf4dd8bdbd1b32a422b6ff765855282df Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Jun 2016 20:32:32 +0100 Subject: [PATCH 019/136] docs --- README.md | 102 ++++++--------------- example/README.md | 3 + .../{ => built-in}/forbid/.local-web-server.json | 0 example/{ => built-in}/forbid/admin/blocked.html | 0 example/{ => built-in}/forbid/allowed.html | 0 example/{ => built-in}/forbid/index.html | 0 example/{ => built-in}/forbid/something.php | 0 .../mime-override/.local-web-server.json | 0 example/{ => built-in}/mime-override/something.php | 0 .../mock-async/.local-web-server.json | 0 example/{ => built-in}/mock-async/mocks/delayed.js | 0 example/{ => built-in}/mock/.local-web-server.json | 0 example/{ => built-in}/mock/mocks/five.js | 0 example/{ => built-in}/mock/mocks/stream-self.js | 0 example/{ => built-in}/mock/mocks/user.js | 0 example/{ => built-in}/mock/mocks/users.js | 0 example/{ => built-in}/mock/mocks/users.json | 0 .../{ => built-in}/rewrite/.local-web-server.json | 0 .../{ => built-in}/rewrite/build/styles/style.css | 0 example/{ => built-in}/rewrite/index.html | 0 example/{ => built-in}/simple/css/style.css | 0 example/{ => built-in}/simple/index.html | 0 example/{ => built-in}/simple/package.json | 0 example/{ => built-in}/spa/.local-web-server.json | 0 example/{ => built-in}/spa/css/style.css | 0 example/{ => built-in}/spa/image.jpg | Bin example/{ => built-in}/spa/index.html | 0 .../custom}/cache-control/.local-web-server.json | 0 {extend => example/custom}/cache-control/server.js | 0 .../custom}/live-reload-optional/index.html | 0 .../custom}/live-reload-optional/server.js | 0 {extend => example/custom}/live-reload/index.html | 0 {extend => example/custom}/live-reload/server.js | 0 jsdoc2md/README.hbs | 42 ++++++--- 34 files changed, 59 insertions(+), 88 deletions(-) create mode 100644 example/README.md rename example/{ => built-in}/forbid/.local-web-server.json (100%) rename example/{ => built-in}/forbid/admin/blocked.html (100%) rename example/{ => built-in}/forbid/allowed.html (100%) rename example/{ => built-in}/forbid/index.html (100%) rename example/{ => built-in}/forbid/something.php (100%) rename example/{ => built-in}/mime-override/.local-web-server.json (100%) rename example/{ => built-in}/mime-override/something.php (100%) rename example/{ => built-in}/mock-async/.local-web-server.json (100%) rename example/{ => built-in}/mock-async/mocks/delayed.js (100%) rename example/{ => built-in}/mock/.local-web-server.json (100%) rename example/{ => built-in}/mock/mocks/five.js (100%) rename example/{ => built-in}/mock/mocks/stream-self.js (100%) rename example/{ => built-in}/mock/mocks/user.js (100%) rename example/{ => built-in}/mock/mocks/users.js (100%) rename example/{ => built-in}/mock/mocks/users.json (100%) rename example/{ => built-in}/rewrite/.local-web-server.json (100%) rename example/{ => built-in}/rewrite/build/styles/style.css (100%) rename example/{ => built-in}/rewrite/index.html (100%) rename example/{ => built-in}/simple/css/style.css (100%) rename example/{ => built-in}/simple/index.html (100%) rename example/{ => built-in}/simple/package.json (100%) rename example/{ => built-in}/spa/.local-web-server.json (100%) rename example/{ => built-in}/spa/css/style.css (100%) rename example/{ => built-in}/spa/image.jpg (100%) rename example/{ => built-in}/spa/index.html (100%) rename {extend => example/custom}/cache-control/.local-web-server.json (100%) rename {extend => example/custom}/cache-control/server.js (100%) rename {extend => example/custom}/live-reload-optional/index.html (100%) rename {extend => example/custom}/live-reload-optional/server.js (100%) rename {extend => example/custom}/live-reload/index.html (100%) rename {extend => example/custom}/live-reload/server.js (100%) diff --git a/README.md b/README.md index 24ea340..c261713 100644 --- a/README.md +++ b/README.md @@ -8,28 +8,40 @@ ***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.*** # local-web-server -A simple, extensible web-server for productive front-end development. Typical use cases: +An application shell for building a simple, command-line web server for productive web development. -* Front-end Development - * Static or Single Page App development - * Re-route paths to local or remote resources - * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) +It is trivial is bundle and deploy with your project. Also deploys to heroku well for demo projects. + +It comes with some middleware built-in, which you need not use but will get you up and running for the following use cases: + +* Static or Single Page Application front-end development where you have + * No backend, an existing remote API or need to mock-up an API. + +Application Shell + * HTTP or HTTPS server + * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.) + * Add your middleware + * Use any combination of built-in and custom middleware + * specify options (for command line or config) + * Accepts Koa v1 or 2 middleware * Bundle with your front-end project - * Very little configuration, just a few options + * Configuration is via json file or command-line (latter taking presedence) * Outputs a dynamic statistics view to the terminal + + +Built-in Middleware (all optional) + * Rewrite routes to local or remote resources + * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md) -* Back-end service mocking - * Prototype a web service, microservice, REST API etc. - * Mocks are defined with config (static), or code (dynamic). + * Proxy server + * Map local routes to remote servers. Removes CORS pain when consuming remote services. + * Back-end service mocking + * Prototype a web service, microservice, REST API etc. + * Mocks are defined with config (static), or code (dynamic). * CORS-friendly, all origins allowed by default. -* Proxy server - * Map local routes to remote servers. Removes CORS pain when consuming remote services. -* HTTPS server - * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.) -* File sharing ## Synopsis -local-web-server is a simple command-line tool. To use it, from your project directory run `ws`. +local-web-server is a command-line tool. To serve the current directory, run `ws`.
    $ ws --help
     
    @@ -84,7 +96,7 @@ For the examples below, we assume we're in a project directory looking like this
     └── package.json
     ```
     
    -**All paths/routes are specified using [express syntax](http://expressjs.com/guide/routing.html#route-paths)**. To run the example projects linked below, clone the project, move into the example directory specified, run `ws`.
    +All paths/routes are specified using [express syntax](http://expressjs.com/guide/routing.html#route-paths). To run the example projects linked below, clone the project, move into the example directory specified, run `ws`.
     
     ### Static site
     
    @@ -585,63 +597,7 @@ serving at http://localhost:8100
     
     ## API Reference
     
    -
    -* [local-web-server](#module_local-web-server)
    -    * [localWebServer([options])](#exp_module_local-web-server--localWebServer) ⇒ [KoaApplication](https://github.com/koajs/koa/blob/master/docs/api/index.md#application) ⏏
    -        * [~rewriteRule](#module_local-web-server--localWebServer..rewriteRule)
    -
    -
    -### localWebServer([options]) ⇒ [KoaApplication](https://github.com/koajs/koa/blob/master/docs/api/index.md#application) ⏏
    -Returns a Koa application you can launch or mix into an existing app.
    -
    -**Kind**: Exported function  
    -**Params**
    -
    -- [options] object - options
    -    - [.static] object - koa-static config
    -        - [.root] string  = "." - root directory
    -        - [.options] string - [options](https://github.com/koajs/static#options)
    -    - [.serveIndex] object - koa-serve-index config
    -        - [.path] string  = "." - root directory
    -        - [.options] string - [options](https://github.com/expressjs/serve-index#options)
    -    - [.forbid] Array.<string> - A list of forbidden routes, each route being an [express route-path](http://expressjs.com/guide/routing.html#route-paths).
    -    - [.spa] string - specify an SPA file to catch requests for everything but static assets.
    -    - [.log] object - [morgan](https://github.com/expressjs/morgan) config
    -        - [.format] string - [log format](https://github.com/expressjs/morgan#predefined-formats)
    -        - [.options] object - [options](https://github.com/expressjs/morgan#options)
    -    - [.compress] boolean - Serve gzip-compressed resources, where applicable
    -    - [.mime] object - A list of mime-type overrides, passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine)
    -    - [.rewrite] [Array.<rewriteRule>](#module_local-web-server--localWebServer..rewriteRule) - One or more rewrite rules
    -    - [.verbose] boolean - Print detailed output, useful for debugging
    -
    -**Example**  
    -```js
    -const localWebServer = require('local-web-server')
    -localWebServer().listen(8000)
    -```
    -
    -#### localWebServer~rewriteRule
    -The `from` and `to` routes are specified using [express route-paths](http://expressjs.com/guide/routing.html#route-paths)
    -
    -**Kind**: inner typedef of [localWebServer](#exp_module_local-web-server--localWebServer)  
    -**Properties**
    -
    -| Name | Type | Description |
    -| --- | --- | --- |
    -| from | string | request route |
    -| to | string | target route |
    -
    -**Example**  
    -```json
    -{
    -  "rewrite": [
    -    { "from": "/css/*", "to": "/build/styles/$1" },
    -    { "from": "/npm/*", "to": "http://registry.npmjs.org/$1" },
    -    { "from": "/:user/repos/:name", "to": "https://api.github.com/repos/:user/:name" }
    -  ]
    -}
    -```
    -
    +ERROR, Cannot find module.
     * * *
     
     © 2013-16 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown).
    diff --git a/example/README.md b/example/README.md
    new file mode 100644
    index 0000000..0e12d08
    --- /dev/null
    +++ b/example/README.md
    @@ -0,0 +1,3 @@
    +# Examples
    +
    +Some examples of how to use the built-in middleware and configure custom servers.
    diff --git a/example/forbid/.local-web-server.json b/example/built-in/forbid/.local-web-server.json
    similarity index 100%
    rename from example/forbid/.local-web-server.json
    rename to example/built-in/forbid/.local-web-server.json
    diff --git a/example/forbid/admin/blocked.html b/example/built-in/forbid/admin/blocked.html
    similarity index 100%
    rename from example/forbid/admin/blocked.html
    rename to example/built-in/forbid/admin/blocked.html
    diff --git a/example/forbid/allowed.html b/example/built-in/forbid/allowed.html
    similarity index 100%
    rename from example/forbid/allowed.html
    rename to example/built-in/forbid/allowed.html
    diff --git a/example/forbid/index.html b/example/built-in/forbid/index.html
    similarity index 100%
    rename from example/forbid/index.html
    rename to example/built-in/forbid/index.html
    diff --git a/example/forbid/something.php b/example/built-in/forbid/something.php
    similarity index 100%
    rename from example/forbid/something.php
    rename to example/built-in/forbid/something.php
    diff --git a/example/mime-override/.local-web-server.json b/example/built-in/mime-override/.local-web-server.json
    similarity index 100%
    rename from example/mime-override/.local-web-server.json
    rename to example/built-in/mime-override/.local-web-server.json
    diff --git a/example/mime-override/something.php b/example/built-in/mime-override/something.php
    similarity index 100%
    rename from example/mime-override/something.php
    rename to example/built-in/mime-override/something.php
    diff --git a/example/mock-async/.local-web-server.json b/example/built-in/mock-async/.local-web-server.json
    similarity index 100%
    rename from example/mock-async/.local-web-server.json
    rename to example/built-in/mock-async/.local-web-server.json
    diff --git a/example/mock-async/mocks/delayed.js b/example/built-in/mock-async/mocks/delayed.js
    similarity index 100%
    rename from example/mock-async/mocks/delayed.js
    rename to example/built-in/mock-async/mocks/delayed.js
    diff --git a/example/mock/.local-web-server.json b/example/built-in/mock/.local-web-server.json
    similarity index 100%
    rename from example/mock/.local-web-server.json
    rename to example/built-in/mock/.local-web-server.json
    diff --git a/example/mock/mocks/five.js b/example/built-in/mock/mocks/five.js
    similarity index 100%
    rename from example/mock/mocks/five.js
    rename to example/built-in/mock/mocks/five.js
    diff --git a/example/mock/mocks/stream-self.js b/example/built-in/mock/mocks/stream-self.js
    similarity index 100%
    rename from example/mock/mocks/stream-self.js
    rename to example/built-in/mock/mocks/stream-self.js
    diff --git a/example/mock/mocks/user.js b/example/built-in/mock/mocks/user.js
    similarity index 100%
    rename from example/mock/mocks/user.js
    rename to example/built-in/mock/mocks/user.js
    diff --git a/example/mock/mocks/users.js b/example/built-in/mock/mocks/users.js
    similarity index 100%
    rename from example/mock/mocks/users.js
    rename to example/built-in/mock/mocks/users.js
    diff --git a/example/mock/mocks/users.json b/example/built-in/mock/mocks/users.json
    similarity index 100%
    rename from example/mock/mocks/users.json
    rename to example/built-in/mock/mocks/users.json
    diff --git a/example/rewrite/.local-web-server.json b/example/built-in/rewrite/.local-web-server.json
    similarity index 100%
    rename from example/rewrite/.local-web-server.json
    rename to example/built-in/rewrite/.local-web-server.json
    diff --git a/example/rewrite/build/styles/style.css b/example/built-in/rewrite/build/styles/style.css
    similarity index 100%
    rename from example/rewrite/build/styles/style.css
    rename to example/built-in/rewrite/build/styles/style.css
    diff --git a/example/rewrite/index.html b/example/built-in/rewrite/index.html
    similarity index 100%
    rename from example/rewrite/index.html
    rename to example/built-in/rewrite/index.html
    diff --git a/example/simple/css/style.css b/example/built-in/simple/css/style.css
    similarity index 100%
    rename from example/simple/css/style.css
    rename to example/built-in/simple/css/style.css
    diff --git a/example/simple/index.html b/example/built-in/simple/index.html
    similarity index 100%
    rename from example/simple/index.html
    rename to example/built-in/simple/index.html
    diff --git a/example/simple/package.json b/example/built-in/simple/package.json
    similarity index 100%
    rename from example/simple/package.json
    rename to example/built-in/simple/package.json
    diff --git a/example/spa/.local-web-server.json b/example/built-in/spa/.local-web-server.json
    similarity index 100%
    rename from example/spa/.local-web-server.json
    rename to example/built-in/spa/.local-web-server.json
    diff --git a/example/spa/css/style.css b/example/built-in/spa/css/style.css
    similarity index 100%
    rename from example/spa/css/style.css
    rename to example/built-in/spa/css/style.css
    diff --git a/example/spa/image.jpg b/example/built-in/spa/image.jpg
    similarity index 100%
    rename from example/spa/image.jpg
    rename to example/built-in/spa/image.jpg
    diff --git a/example/spa/index.html b/example/built-in/spa/index.html
    similarity index 100%
    rename from example/spa/index.html
    rename to example/built-in/spa/index.html
    diff --git a/extend/cache-control/.local-web-server.json b/example/custom/cache-control/.local-web-server.json
    similarity index 100%
    rename from extend/cache-control/.local-web-server.json
    rename to example/custom/cache-control/.local-web-server.json
    diff --git a/extend/cache-control/server.js b/example/custom/cache-control/server.js
    similarity index 100%
    rename from extend/cache-control/server.js
    rename to example/custom/cache-control/server.js
    diff --git a/extend/live-reload-optional/index.html b/example/custom/live-reload-optional/index.html
    similarity index 100%
    rename from extend/live-reload-optional/index.html
    rename to example/custom/live-reload-optional/index.html
    diff --git a/extend/live-reload-optional/server.js b/example/custom/live-reload-optional/server.js
    similarity index 100%
    rename from extend/live-reload-optional/server.js
    rename to example/custom/live-reload-optional/server.js
    diff --git a/extend/live-reload/index.html b/example/custom/live-reload/index.html
    similarity index 100%
    rename from extend/live-reload/index.html
    rename to example/custom/live-reload/index.html
    diff --git a/extend/live-reload/server.js b/example/custom/live-reload/server.js
    similarity index 100%
    rename from extend/live-reload/server.js
    rename to example/custom/live-reload/server.js
    diff --git a/jsdoc2md/README.hbs b/jsdoc2md/README.hbs
    index 4f0862c..e28d4ce 100644
    --- a/jsdoc2md/README.hbs
    +++ b/jsdoc2md/README.hbs
    @@ -8,28 +8,40 @@
     ***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.***
     
     # local-web-server
    -A simple web-server for productive front-end development. Typical use cases:
    +An application shell for building a simple, command-line web server for productive web development.
     
    -* Front-end Development
    -  * Static or Single Page App development
    -  * Re-route paths to local or remote resources
    -  * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down)
    +It is trivial is bundle and deploy with your project. Also deploys to heroku well for demo projects.
    +
    +It comes with some middleware built-in, which you need not use but will get you up and running for the following use cases:
    +
    +* Static or Single Page Application front-end development where you have
    +  * No backend, an existing remote API or need to mock-up an API.
    +
    +Application Shell
    +  * HTTP or HTTPS server
    +    * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.)
    +  * Add your middleware
    +    * Use any combination of built-in and custom middleware
    +    * specify options (for command line or config)
    +    * Accepts Koa v1 or 2 middleware
       * Bundle with your front-end project
    -  * Very little configuration, just a few options
    +  * Configuration is via json file or command-line (latter taking presedence)
       * Outputs a dynamic statistics view to the terminal
    +
    +
    +Built-in Middleware (all optional)
    +  * Rewrite routes to local or remote resources
    +  * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down)
       * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md)
    -* Back-end service mocking
    -  * Prototype a web service, microservice, REST API etc.
    -  * Mocks are defined with config (static), or code (dynamic).
    +  * Proxy server
    +    * Map local routes to remote servers. Removes CORS pain when consuming remote services.
    +  * Back-end service mocking
    +    * Prototype a web service, microservice, REST API etc.
    +    * Mocks are defined with config (static), or code (dynamic).
       * CORS-friendly, all origins allowed by default.
    -* Proxy server
    -  * Map local routes to remote servers. Removes CORS pain when consuming remote services.
    -* HTTPS server
    -  * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.)
    -* File sharing
     
     ## Synopsis
    -local-web-server is a simple command-line tool. To use it, from your project directory run `ws`.
    +local-web-server is a command-line tool. To serve the current directory, run `ws`.
     
     
    $ ws --help
     
    
    From ef5401d4c6108679fef7ebf2a0d929410ca9db07 Mon Sep 17 00:00:00 2001
    From: Lloyd Brookes 
    Date: Mon, 20 Jun 2016 22:27:55 +0100
    Subject: [PATCH 020/136] docs
    
    ---
     README.md               | 446 ++++--------------------------------------------
     doc/blacklist.md        |   9 +
     doc/https.md            |  59 +++++++
     doc/logging.md          |  69 ++++++++
     doc/mime-types.md       |  12 ++
     doc/mock-response.md    | 241 ++++++++++++++++++++++++++
     doc/rewrite.md          |  37 ++++
     doc/spa.md              |  12 ++
     doc/stored-config.md    |  28 +++
     jsdoc2md/README.hbs     | 416 --------------------------------------------
     lib/default-stack.js    | 291 +++++++++++++++++++++++++++++++
     lib/local-web-server.js |  17 +-
     lib/middleware-stack.js | 294 ++-----------------------------
     13 files changed, 817 insertions(+), 1114 deletions(-)
     create mode 100644 doc/blacklist.md
     create mode 100644 doc/https.md
     create mode 100644 doc/logging.md
     create mode 100644 doc/mime-types.md
     create mode 100644 doc/mock-response.md
     create mode 100644 doc/rewrite.md
     create mode 100644 doc/spa.md
     create mode 100644 doc/stored-config.md
     create mode 100644 lib/default-stack.js
    
    diff --git a/README.md b/README.md
    index c261713..237b648 100644
    --- a/README.md
    +++ b/README.md
    @@ -108,409 +108,6 @@ serving at http://localhost:8000
     
     [Example](https://github.com/75lb/local-web-server/tree/master/example/simple).
     
    -### Single Page Application
    -
    -You're building a web app with client-side routing, so mark `index.html` as the SPA.
    -```sh
    -$ ws --spa index.html
    -```
    -
    -By default, typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not Found` as a file does not exist with that path. By marking `index.html` as the SPA you create this rule:
    -
    -*If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.*
    -
    -[Example](https://github.com/75lb/local-web-server/tree/master/example/spa).
    -
    -### URL rewriting
    -
    -Your application requested `/css/style.css` but it's stored at `/build/css/style.css`. To avoid a 404 you need a rewrite rule:
    -
    -```sh
    -$ ws --rewrite '/css/style.css -> /build/css/style.css'
    -```
    -
    -Or, more generally (matching any stylesheet under `/css`):
    -
    -```sh
    -$ ws --rewrite '/css/:stylesheet -> /build/css/:stylesheet'
    -```
    -
    -With a deep CSS directory structure it may be easier to mount the entire contents of `/build/css` to the `/css` path:
    -
    -```sh
    -$ ws --rewrite '/css/* -> /build/css/$1'
    -```
    -
    -this rewrites `/css/a` as `/build/css/a`, `/css/a/b/c` as `/build/css/a/b/c` etc.
    -
    -#### Proxied requests
    -
    -If the `to` URL contains a remote host, local-web-server will act as a proxy - fetching and responding with the remote resource.
    -
    -Mount the npm registry locally:
    -```sh
    -$ ws --rewrite '/npm/* -> http://registry.npmjs.org/$1'
    -```
    -
    -Map local requests for repo data to the Github API:
    -```sh
    -$ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name'
    -```
    -
    -[Example](https://github.com/75lb/local-web-server/tree/master/example/rewrite).
    -
    -### Mock Responses
    -
    -Mocks give you full control over the response headers and body returned to the client. They can be used to return anything from a simple html string to a resourceful REST API. Typically, they're used to mock services but can be used for anything.
    -
    -In the config, define an array called `mocks`. Each mock definition maps a [route](http://expressjs.com/guide/routing.html#route-paths) to a `response`. A simple home page:
    -```json
    -{
    -  "mocks": [
    -    {
    -      "route": "/",
    -      "response": {
    -        "body": "

    Welcome to the Mock Responses example

    " - } - } - ] -} -``` - -Under the hood, the property values from the `response` object are written onto the underlying [koa response object](https://github.com/koajs/koa/blob/master/docs/api/response.md). You can set any valid koa response properies, for example [type](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsetype-1): -```json -{ - "mocks": [ - { - "route": "/", - "response": { - "type": "text/plain", - "body": "

    Welcome to the Mock Responses example

    " - } - } - ] -} -``` - -#### Conditional Response - -To define a conditional response, set a `request` object on the mock definition. The `request` value acts as a query - the response defined will only be returned if each property of the `request` query matches. For example, return an XML response *only* if the request headers include `accept: application/xml`, else return 404 Not Found. - -```json -{ - "mocks": [ - { - "route": "/two", - "request": { "accepts": "xml" }, - "response": { - "body": "" - } - } - ] -} -``` - -#### Multiple Potential Responses - -To specify multiple potential responses, set an array of mock definitions to the `responses` property. The first response with a matching request query will be sent. In this example, the client will get one of two responses depending on the request method: - -```json -{ - "mocks": [ - { - "route": "/three", - "responses": [ - { - "request": { "method": "GET" }, - "response": { - "body": "

    Mock response for 'GET' request on /three

    " - } - }, - { - "request": { "method": "POST" }, - "response": { - "status": 400, - "body": { "message": "That method is not allowed." } - } - } - ] - } - ] -} -``` - -#### Dynamic Response - -The examples above all returned static data. To define a dynamic response, create a mock module. Specify its path in the `module` property: -```json -{ - "mocks": [ - { - "route": "/four", - "module": "/mocks/stream-self.js" - } - ] -} -``` - -Here's what the `stream-self` module looks like. The module should export a mock definition (an object, or array of objects, each with a `response` and optional `request`). In this example, the module simply streams itself to the response but you could set `body` to *any* [valid value](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsebody-1). -```js -const fs = require('fs') - -module.exports = { - response: { - body: fs.createReadStream(__filename) - } -} -``` - -#### Response function - -For more power, define the response as a function. It will receive the [koa context](https://github.com/koajs/koa/blob/master/docs/api/context.md) as its first argument. Now you have full programmatic control over the response returned. -```js -module.exports = { - response: function (ctx) { - ctx.body = '

    I can do anything i want.

    ' - } -} -``` - -If the route contains tokens, their values are passed to the response. For example, with this mock... -```json -{ - "mocks": [ - { - "route": "/players/:id", - "module": "/mocks/players.js" - } - ] -} -``` - -...the `id` value is passed to the `response` function. For example, a path of `/players/10?name=Lionel` would pass `10` to the response function. Additional, the value `Lionel` would be available on `ctx.query.name`: -```js -module.exports = { - response: function (ctx, id) { - ctx.body = `

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

    ` - } -} -``` - -#### RESTful Resource example - -Here's an example of a REST collection (users). We'll create two routes, one for actions on the resource collection, one for individual resource actions. - -```json -{ - "mocks": [ - { "route": "/users", "module": "/mocks/users.js" }, - { "route": "/users/:id", "module": "/mocks/user.js" } - ] -} -``` - -Define a module (`users.json`) defining seed data: - -```json -[ - { "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, - { "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, - { "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } -] -``` - -The collection module: - -```js -const users = require('./users.json') - -/* responses for /users */ -const mockResponses = [ - /* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ - { request: { method: 'PUT' }, response: { status: 400 } }, - { request: { method: 'DELETE' }, response: { status: 400 } }, - { - /* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ - request: { method: 'GET' }, - response: function (ctx) { - ctx.body = users.filter(user => { - const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) - const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) - return meetsMinAge && requiredNationality - }) - } - }, - { - /* for POST requests, create a new user and return the path to the new resource */ - request: { method: 'POST' }, - response: function (ctx) { - const newUser = ctx.request.body - users.push(newUser) - newUser.id = users.length - ctx.status = 201 - ctx.response.set('Location', `/users/${newUser.id}`) - } - } -] - -module.exports = mockResponses -``` - -The individual resource module: - -```js -const users = require('./users.json') - -/* responses for /users/:id */ -const mockResponses = [ - /* don't support POST here */ - { request: { method: 'POST' }, response: { status: 400 } }, - - /* for GET requests, return a particular user */ - { - request: { method: 'GET' }, - response: function (ctx, id) { - ctx.body = users.find(user => user.id === Number(id)) - } - }, - - /* for PUT requests, update the record */ - { - request: { method: 'PUT' }, - response: function (ctx, id) { - const updatedUser = ctx.request.body - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1, updatedUser) - ctx.status = 200 - } - }, - - /* DELETE request: remove the record */ - { - request: { method: 'DELETE' }, - response: function (ctx, id) { - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1) - ctx.status = 200 - } - } -] - -module.exports = mockResponses -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/mock). - -### HTTPS Server - -Some modern techs (ServiceWorker, any `MediaDevices.getUserMedia()` request etc.) *must* be served from a secure origin (HTTPS). To launch an HTTPS server, supply a `--key` and `--cert` to local-web-server, for example: - -``` -$ ws --key localhost.key --cert localhost.crt -``` - -If you don't have a key and certificate it's trivial to create them. You do not need third-party verification (Verisign etc.) for development purposes. To get the green padlock in the browser, the certificate.. - -* must have a `Common Name` value matching the FQDN of the server -* must be verified by a Certificate Authority (but we can overrule this - see below) - -First create a certificate: - -1. Install openssl. - - `$ brew install openssl` - -2. Generate a RSA private key. - - `$ openssl genrsa -des3 -passout pass:x -out ws.pass.key 2048` - -3. Create RSA key. - - ``` - $ openssl rsa -passin pass:x -in ws.pass.key -out ws.key - ``` - -4. Create certificate request. The command below will ask a series of questions about the certificate owner. The most imporant answer to give is for `Common Name`, you can accept the default values for the others. **Important**: you **must** input your server's correct FQDN (`dev-server.local`, `laptop.home` etc.) into the `Common Name` field. The cert is only valid for the domain specified here. You can find out your computers host name by running the command `hostname`. For example, mine is `mba3.home`. - - `$ openssl req -new -key ws.key -out ws.csr` - -5. Generate self-signed certificate. - - `$ openssl x509 -req -days 365 -in ws.csr -signkey ws.key -out ws.crt` - -6. Clean up files we're finished with - - `$ rm ws.pass.key ws.csr` - -7. Launch HTTPS server. In iTerm, control-click the first URL (with the hostname matching `Common Name`) to launch your browser. - - ``` - $ ws --key ws.key --cert ws.crt - serving at https://mba3.home:8010, https://127.0.0.1:8010, https://192.168.1.203:8010 - ``` - -Chrome and Firefox will still complain your certificate has not been verified by a Certificate Authority. Firefox will offer you an `Add an exception` option, allowing you to ignore the warning and manually mark the certificate as trusted. In Chrome on Mac, you can manually trust the certificate another way: - -1. Open Keychain -2. Click File -> Import. Select the `.crt` file you created. -3. In the `Certificates` category, double-click the cert you imported. -4. In the `trust` section, underneath `when using this certificate`, select `Always Trust`. - -Now you have a valid, trusted certificate for development. - -#### Built-in certificate -As a quick win, you can run `ws` with the `https` flag. This will launch an HTTPS server using a [built-in certificate](https://github.com/75lb/local-web-server/tree/master/ssl) registered to the domain 127.0.0.1. - -### Stored config - -Use the same options every time? Persist then to `package.json`: -```json -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100, - "forbid": "*.json" - } -} -``` - -or `.local-web-server.json` -```json -{ - "port": 8100, - "forbid": "*.json" -} -``` - -local-web-server will merge and use all config found, searching from the current directory upward. In the case both `package.json` and `.local-web-server.json` config is found in the same directory, `.local-web-server.json` will take precedence. Options set on the command line take precedence over all. - -To inspect stored config, run: -```sh -$ ws --config -``` - -### Logging -By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log-format`: - -```sh -$ ws --log-format combined -serving at http://localhost:8000 -::1 - - [16/Nov/2015:11:16:52 +0000] "GET / HTTP/1.1" 200 12290 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2562.0 Safari/537.36" -``` - -The format value supplied is passed directly to [morgan](https://github.com/expressjs/morgan). The exception is `--log-format none` which disables all output. - -### Access Control - -By default, access to all files is allowed (including dot files). Use `--forbid` to establish a blacklist: -```sh -$ ws --forbid '*.json' '*.yml' -serving at http://localhost:8000 -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/forbid). - ### Other usage #### Debugging @@ -534,19 +131,6 @@ Disable etag response headers, forcing resources to be served in full every time $ ws --no-cache ``` -#### mime-types -You can set additional mime-type/extension mappings, or override the defaults by setting a `mime` value in the stored config. This value is passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine). Example: - -```json -{ - "mime": { - "text/plain": [ "php", "pl" ] - } -} -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/mime-override). - #### Log Visualisation Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md). @@ -597,7 +181,35 @@ serving at http://localhost:8100 ## API Reference -ERROR, Cannot find module. + +* [local-web-server](#module_local-web-server) + * [LocalWebServer](#exp_module_local-web-server--LocalWebServer) ⇐ [middleware-stack](#module_middleware-stack) ⏏ + * _instance_ + * [.add(middleware)](#) ↩︎ + * _inner_ + * [~collectUserOptions()](#module_local-web-server--LocalWebServer..collectUserOptions) + + + +### LocalWebServer ⇐ [middleware-stack](#module_middleware-stack) ⏏ +**Kind**: Exported class +**Extends:** [middleware-stack](#module_middleware-stack) + + +#### localWebServer.add(middleware) ↩︎ +**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) +**Chainable** +**Params** + +- middleware [middleware](#module_middleware-stack--MiddlewareStack..middleware) + + + +#### LocalWebServer~collectUserOptions() +Return default, stored and command-line options combined + +**Kind**: inner method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + * * * © 2013-16 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). diff --git a/doc/blacklist.md b/doc/blacklist.md new file mode 100644 index 0000000..6793d50 --- /dev/null +++ b/doc/blacklist.md @@ -0,0 +1,9 @@ +## Access Control + +By default, access to all files is allowed (including dot files). Use `--forbid` to establish a blacklist: +```sh +$ ws --forbid '*.json' '*.yml' +serving at http://localhost:8000 +``` + +[Example](https://github.com/75lb/local-web-server/tree/master/example/forbid). diff --git a/doc/https.md b/doc/https.md new file mode 100644 index 0000000..8802915 --- /dev/null +++ b/doc/https.md @@ -0,0 +1,59 @@ +## HTTPS Server + +Some modern techs (ServiceWorker, any `MediaDevices.getUserMedia()` request etc.) *must* be served from a secure origin (HTTPS). To launch an HTTPS server, supply a `--key` and `--cert` to local-web-server, for example: + +``` +$ ws --key localhost.key --cert localhost.crt +``` + +If you don't have a key and certificate it's trivial to create them. You do not need third-party verification (Verisign etc.) for development purposes. To get the green padlock in the browser, the certificate.. + +* must have a `Common Name` value matching the FQDN of the server +* must be verified by a Certificate Authority (but we can overrule this - see below) + +First create a certificate: + +1. Install openssl. + + `$ brew install openssl` + +2. Generate a RSA private key. + + `$ openssl genrsa -des3 -passout pass:x -out ws.pass.key 2048` + +3. Create RSA key. + + ``` + $ openssl rsa -passin pass:x -in ws.pass.key -out ws.key + ``` + +4. Create certificate request. The command below will ask a series of questions about the certificate owner. The most imporant answer to give is for `Common Name`, you can accept the default values for the others. **Important**: you **must** input your server's correct FQDN (`dev-server.local`, `laptop.home` etc.) into the `Common Name` field. The cert is only valid for the domain specified here. You can find out your computers host name by running the command `hostname`. For example, mine is `mba3.home`. + + `$ openssl req -new -key ws.key -out ws.csr` + +5. Generate self-signed certificate. + + `$ openssl x509 -req -days 365 -in ws.csr -signkey ws.key -out ws.crt` + +6. Clean up files we're finished with + + `$ rm ws.pass.key ws.csr` + +7. Launch HTTPS server. In iTerm, control-click the first URL (with the hostname matching `Common Name`) to launch your browser. + + ``` + $ ws --key ws.key --cert ws.crt + serving at https://mba3.home:8010, https://127.0.0.1:8010, https://192.168.1.203:8010 + ``` + +Chrome and Firefox will still complain your certificate has not been verified by a Certificate Authority. Firefox will offer you an `Add an exception` option, allowing you to ignore the warning and manually mark the certificate as trusted. In Chrome on Mac, you can manually trust the certificate another way: + +1. Open Keychain +2. Click File -> Import. Select the `.crt` file you created. +3. In the `Certificates` category, double-click the cert you imported. +4. In the `trust` section, underneath `when using this certificate`, select `Always Trust`. + +Now you have a valid, trusted certificate for development. + +### Built-in certificate +As a quick win, you can run `ws` with the `https` flag. This will launch an HTTPS server using a [built-in certificate](https://github.com/75lb/local-web-server/tree/master/ssl) registered to the domain 127.0.0.1. diff --git a/doc/logging.md b/doc/logging.md new file mode 100644 index 0000000..c41cd21 --- /dev/null +++ b/doc/logging.md @@ -0,0 +1,69 @@ +# Logging +By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log-format`: + +```sh +$ ws --log-format combined +serving at http://localhost:8000 +::1 - - [16/Nov/2015:11:16:52 +0000] "GET / HTTP/1.1" 200 12290 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2562.0 Safari/537.36" +``` + +The format value supplied is passed directly to [morgan](https://github.com/expressjs/morgan). The exception is `--log-format none` which disables all output. + +# Visualisation + +## Goaccess +To get live statistics in [goaccess](http://goaccess.io/), first create this config file at `~/.goaccessrc`: + +``` +time-format %T +date-format %d/%b/%Y +log-format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" +``` + +Then, start the server, outputting `combined` format logs to disk: + +```sh +$ ws -f combined > web.log +``` + +In a separate tab, point goaccess at `web.log` and it will display statistics in real time: + +``` +$ goaccess -p ~/.goaccessrc -f web.log +``` + +## Logstalgia +local-web-server is compatible with [logstalgia](http://code.google.com/p/logstalgia/). + +### Install Logstalgia +On MacOSX, install with [homebrew](http://brew.sh): +```sh +$ brew install logstalgia +``` + +Alternatively, [download a release for your system from github](https://github.com/acaudwell/Logstalgia/releases/latest). + +Then pipe the `logstalgia` output format directly into logstalgia for real-time visualisation: +```sh +$ ws -f logstalgia | logstalgia - +``` + +![local-web-server with logstalgia](https://raw.githubusercontent.com/75lb/local-web-server/master/doc/img/logstagia.gif) + +## glTail +To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format: +```sh +$ ws -f default > web.log +``` + +Then specify this file in your glTail config: + +```yaml +servers: + dev: + host: localhost + source: local + files: /Users/Lloyd/Documents/MySite/web.log + parser: apache + color: 0.2, 0.2, 1.0, 1.0 +``` diff --git a/doc/mime-types.md b/doc/mime-types.md new file mode 100644 index 0000000..3251230 --- /dev/null +++ b/doc/mime-types.md @@ -0,0 +1,12 @@ +## mime-types +You can set additional mime-type/extension mappings, or override the defaults by setting a `mime` value in the stored config. This value is passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine). Example: + +```json +{ + "mime": { + "text/plain": [ "php", "pl" ] + } +} +``` + +[Example](https://github.com/75lb/local-web-server/tree/master/example/mime-override). diff --git a/doc/mock-response.md b/doc/mock-response.md new file mode 100644 index 0000000..2d2b796 --- /dev/null +++ b/doc/mock-response.md @@ -0,0 +1,241 @@ +## Mock Responses + +Mocks give you full control over the response headers and body returned to the client. They can be used to return anything from a simple html string to a resourceful REST API. Typically, they're used to mock services but can be used for anything. + +In the config, define an array called `mocks`. Each mock definition maps a [route](http://expressjs.com/guide/routing.html#route-paths) to a `response`. A simple home page: +```json +{ + "mocks": [ + { + "route": "/", + "response": { + "body": "

    Welcome to the Mock Responses example

    " + } + } + ] +} +``` + +Under the hood, the property values from the `response` object are written onto the underlying [koa response object](https://github.com/koajs/koa/blob/master/docs/api/response.md). You can set any valid koa response properies, for example [type](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsetype-1): +```json +{ + "mocks": [ + { + "route": "/", + "response": { + "type": "text/plain", + "body": "

    Welcome to the Mock Responses example

    " + } + } + ] +} +``` + +### Conditional Response + +To define a conditional response, set a `request` object on the mock definition. The `request` value acts as a query - the response defined will only be returned if each property of the `request` query matches. For example, return an XML response *only* if the request headers include `accept: application/xml`, else return 404 Not Found. + +```json +{ + "mocks": [ + { + "route": "/two", + "request": { "accepts": "xml" }, + "response": { + "body": "" + } + } + ] +} +``` + +### Multiple Potential Responses + +To specify multiple potential responses, set an array of mock definitions to the `responses` property. The first response with a matching request query will be sent. In this example, the client will get one of two responses depending on the request method: + +```json +{ + "mocks": [ + { + "route": "/three", + "responses": [ + { + "request": { "method": "GET" }, + "response": { + "body": "

    Mock response for 'GET' request on /three

    " + } + }, + { + "request": { "method": "POST" }, + "response": { + "status": 400, + "body": { "message": "That method is not allowed." } + } + } + ] + } + ] +} +``` + +### Dynamic Response + +The examples above all returned static data. To define a dynamic response, create a mock module. Specify its path in the `module` property: +```json +{ + "mocks": [ + { + "route": "/four", + "module": "/mocks/stream-self.js" + } + ] +} +``` + +Here's what the `stream-self` module looks like. The module should export a mock definition (an object, or array of objects, each with a `response` and optional `request`). In this example, the module simply streams itself to the response but you could set `body` to *any* [valid value](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsebody-1). +```js +const fs = require('fs') + +module.exports = { + response: { + body: fs.createReadStream(__filename) + } +} +``` + +### Response function + +For more power, define the response as a function. It will receive the [koa context](https://github.com/koajs/koa/blob/master/docs/api/context.md) as its first argument. Now you have full programmatic control over the response returned. +```js +module.exports = { + response: function (ctx) { + ctx.body = '

    I can do anything i want.

    ' + } +} +``` + +If the route contains tokens, their values are passed to the response. For example, with this mock... +```json +{ + "mocks": [ + { + "route": "/players/:id", + "module": "/mocks/players.js" + } + ] +} +``` + +...the `id` value is passed to the `response` function. For example, a path of `/players/10?name=Lionel` would pass `10` to the response function. Additional, the value `Lionel` would be available on `ctx.query.name`: +```js +module.exports = { + response: function (ctx, id) { + ctx.body = `

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

    ` + } +} +``` + +### RESTful Resource example + +Here's an example of a REST collection (users). We'll create two routes, one for actions on the resource collection, one for individual resource actions. + +```json +{ + "mocks": [ + { "route": "/users", "module": "/mocks/users.js" }, + { "route": "/users/:id", "module": "/mocks/user.js" } + ] +} +``` + +Define a module (`users.json`) defining seed data: + +```json +[ + { "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, + { "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, + { "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } +] +``` + +The collection module: + +```js +const users = require('./users.json') + +/* responses for /users */ +const mockResponses = [ + /* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ + { request: { method: 'PUT' }, response: { status: 400 } }, + { request: { method: 'DELETE' }, response: { status: 400 } }, + { + /* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ + request: { method: 'GET' }, + response: function (ctx) { + ctx.body = users.filter(user => { + const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) + const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) + return meetsMinAge && requiredNationality + }) + } + }, + { + /* for POST requests, create a new user and return the path to the new resource */ + request: { method: 'POST' }, + response: function (ctx) { + const newUser = ctx.request.body + users.push(newUser) + newUser.id = users.length + ctx.status = 201 + ctx.response.set('Location', `/users/${newUser.id}`) + } + } +] + +module.exports = mockResponses +``` + +The individual resource module: + +```js +const users = require('./users.json') + +/* responses for /users/:id */ +const mockResponses = [ + /* don't support POST here */ + { request: { method: 'POST' }, response: { status: 400 } }, + + /* for GET requests, return a particular user */ + { + request: { method: 'GET' }, + response: function (ctx, id) { + ctx.body = users.find(user => user.id === Number(id)) + } + }, + + /* for PUT requests, update the record */ + { + request: { method: 'PUT' }, + response: function (ctx, id) { + const updatedUser = ctx.request.body + const existingUserIndex = users.findIndex(user => user.id === Number(id)) + users.splice(existingUserIndex, 1, updatedUser) + ctx.status = 200 + } + }, + + /* DELETE request: remove the record */ + { + request: { method: 'DELETE' }, + response: function (ctx, id) { + const existingUserIndex = users.findIndex(user => user.id === Number(id)) + users.splice(existingUserIndex, 1) + ctx.status = 200 + } + } +] + +module.exports = mockResponses +``` + +[Example](https://github.com/75lb/local-web-server/tree/master/example/mock). \ No newline at end of file diff --git a/doc/rewrite.md b/doc/rewrite.md new file mode 100644 index 0000000..af1907a --- /dev/null +++ b/doc/rewrite.md @@ -0,0 +1,37 @@ +## URL rewriting + +Your application requested `/css/style.css` but it's stored at `/build/css/style.css`. To avoid a 404 you need a rewrite rule: + +```sh +$ ws --rewrite '/css/style.css -> /build/css/style.css' +``` + +Or, more generally (matching any stylesheet under `/css`): + +```sh +$ ws --rewrite '/css/:stylesheet -> /build/css/:stylesheet' +``` + +With a deep CSS directory structure it may be easier to mount the entire contents of `/build/css` to the `/css` path: + +```sh +$ ws --rewrite '/css/* -> /build/css/$1' +``` + +this rewrites `/css/a` as `/build/css/a`, `/css/a/b/c` as `/build/css/a/b/c` etc. + +### Proxied requests + +If the `to` URL contains a remote host, local-web-server will act as a proxy - fetching and responding with the remote resource. + +Mount the npm registry locally: +```sh +$ ws --rewrite '/npm/* -> http://registry.npmjs.org/$1' +``` + +Map local requests for repo data to the Github API: +```sh +$ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name' +``` + +[Example](https://github.com/75lb/local-web-server/tree/master/example/rewrite). diff --git a/doc/spa.md b/doc/spa.md new file mode 100644 index 0000000..1c277e6 --- /dev/null +++ b/doc/spa.md @@ -0,0 +1,12 @@ +## Single Page Application + +You're building a web app with client-side routing, so mark `index.html` as the SPA. +```sh +$ ws --spa index.html +``` + +By default, typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not Found` as a file does not exist with that path. By marking `index.html` as the SPA you create this rule: + +*If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* + +[Example](https://github.com/75lb/local-web-server/tree/master/example/spa). diff --git a/doc/stored-config.md b/doc/stored-config.md new file mode 100644 index 0000000..3a63ec3 --- /dev/null +++ b/doc/stored-config.md @@ -0,0 +1,28 @@ +## Stored config + +Use the same options every time? Persist then to `package.json`: +```json +{ + "name": "example", + "version": "1.0.0", + "local-web-server": { + "port": 8100, + "forbid": "*.json" + } +} +``` + +or `.local-web-server.json` +```json +{ + "port": 8100, + "forbid": "*.json" +} +``` + +local-web-server will merge and use all config found, searching from the current directory upward. In the case both `package.json` and `.local-web-server.json` config is found in the same directory, `.local-web-server.json` will take precedence. Options set on the command line take precedence over all. + +To inspect stored config, run: +```sh +$ ws --config +``` \ No newline at end of file diff --git a/jsdoc2md/README.hbs b/jsdoc2md/README.hbs index e28d4ce..56d74d1 100644 --- a/jsdoc2md/README.hbs +++ b/jsdoc2md/README.hbs @@ -108,409 +108,6 @@ serving at http://localhost:8000 [Example](https://github.com/75lb/local-web-server/tree/master/example/simple). -### Single Page Application - -You're building a web app with client-side routing, so mark `index.html` as the SPA. -```sh -$ ws --spa index.html -``` - -By default, typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not Found` as a file does not exist with that path. By marking `index.html` as the SPA you create this rule: - -*If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* - -[Example](https://github.com/75lb/local-web-server/tree/master/example/spa). - -### URL rewriting - -Your application requested `/css/style.css` but it's stored at `/build/css/style.css`. To avoid a 404 you need a rewrite rule: - -```sh -$ ws --rewrite '/css/style.css -> /build/css/style.css' -``` - -Or, more generally (matching any stylesheet under `/css`): - -```sh -$ ws --rewrite '/css/:stylesheet -> /build/css/:stylesheet' -``` - -With a deep CSS directory structure it may be easier to mount the entire contents of `/build/css` to the `/css` path: - -```sh -$ ws --rewrite '/css/* -> /build/css/$1' -``` - -this rewrites `/css/a` as `/build/css/a`, `/css/a/b/c` as `/build/css/a/b/c` etc. - -#### Proxied requests - -If the `to` URL contains a remote host, local-web-server will act as a proxy - fetching and responding with the remote resource. - -Mount the npm registry locally: -```sh -$ ws --rewrite '/npm/* -> http://registry.npmjs.org/$1' -``` - -Map local requests for repo data to the Github API: -```sh -$ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name' -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/rewrite). - -### Mock Responses - -Mocks give you full control over the response headers and body returned to the client. They can be used to return anything from a simple html string to a resourceful REST API. Typically, they're used to mock services but can be used for anything. - -In the config, define an array called `mocks`. Each mock definition maps a [route](http://expressjs.com/guide/routing.html#route-paths) to a `response`. A simple home page: -```json -{ - "mocks": [ - { - "route": "/", - "response": { - "body": "

    Welcome to the Mock Responses example

    " - } - } - ] -} -``` - -Under the hood, the property values from the `response` object are written onto the underlying [koa response object](https://github.com/koajs/koa/blob/master/docs/api/response.md). You can set any valid koa response properies, for example [type](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsetype-1): -```json -{ - "mocks": [ - { - "route": "/", - "response": { - "type": "text/plain", - "body": "

    Welcome to the Mock Responses example

    " - } - } - ] -} -``` - -#### Conditional Response - -To define a conditional response, set a `request` object on the mock definition. The `request` value acts as a query - the response defined will only be returned if each property of the `request` query matches. For example, return an XML response *only* if the request headers include `accept: application/xml`, else return 404 Not Found. - -```json -{ - "mocks": [ - { - "route": "/two", - "request": { "accepts": "xml" }, - "response": { - "body": "" - } - } - ] -} -``` - -#### Multiple Potential Responses - -To specify multiple potential responses, set an array of mock definitions to the `responses` property. The first response with a matching request query will be sent. In this example, the client will get one of two responses depending on the request method: - -```json -{ - "mocks": [ - { - "route": "/three", - "responses": [ - { - "request": { "method": "GET" }, - "response": { - "body": "

    Mock response for 'GET' request on /three

    " - } - }, - { - "request": { "method": "POST" }, - "response": { - "status": 400, - "body": { "message": "That method is not allowed." } - } - } - ] - } - ] -} -``` - -#### Dynamic Response - -The examples above all returned static data. To define a dynamic response, create a mock module. Specify its path in the `module` property: -```json -{ - "mocks": [ - { - "route": "/four", - "module": "/mocks/stream-self.js" - } - ] -} -``` - -Here's what the `stream-self` module looks like. The module should export a mock definition (an object, or array of objects, each with a `response` and optional `request`). In this example, the module simply streams itself to the response but you could set `body` to *any* [valid value](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsebody-1). -```js -const fs = require('fs') - -module.exports = { - response: { - body: fs.createReadStream(__filename) - } -} -``` - -#### Response function - -For more power, define the response as a function. It will receive the [koa context](https://github.com/koajs/koa/blob/master/docs/api/context.md) as its first argument. Now you have full programmatic control over the response returned. -```js -module.exports = { - response: function (ctx) { - ctx.body = '

    I can do anything i want.

    ' - } -} -``` - -If the route contains tokens, their values are passed to the response. For example, with this mock... -```json -{ - "mocks": [ - { - "route": "/players/:id", - "module": "/mocks/players.js" - } - ] -} -``` - -...the `id` value is passed to the `response` function. For example, a path of `/players/10?name=Lionel` would pass `10` to the response function. Additional, the value `Lionel` would be available on `ctx.query.name`: -```js -module.exports = { - response: function (ctx, id) { - ctx.body = `

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

    ` - } -} -``` - -#### RESTful Resource example - -Here's an example of a REST collection (users). We'll create two routes, one for actions on the resource collection, one for individual resource actions. - -```json -{ - "mocks": [ - { "route": "/users", "module": "/mocks/users.js" }, - { "route": "/users/:id", "module": "/mocks/user.js" } - ] -} -``` - -Define a module (`users.json`) defining seed data: - -```json -[ - { "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, - { "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, - { "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } -] -``` - -The collection module: - -```js -const users = require('./users.json') - -/* responses for /users */ -const mockResponses = [ - /* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ - { request: { method: 'PUT' }, response: { status: 400 } }, - { request: { method: 'DELETE' }, response: { status: 400 } }, - { - /* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ - request: { method: 'GET' }, - response: function (ctx) { - ctx.body = users.filter(user => { - const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) - const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) - return meetsMinAge && requiredNationality - }) - } - }, - { - /* for POST requests, create a new user and return the path to the new resource */ - request: { method: 'POST' }, - response: function (ctx) { - const newUser = ctx.request.body - users.push(newUser) - newUser.id = users.length - ctx.status = 201 - ctx.response.set('Location', `/users/${newUser.id}`) - } - } -] - -module.exports = mockResponses -``` - -The individual resource module: - -```js -const users = require('./users.json') - -/* responses for /users/:id */ -const mockResponses = [ - /* don't support POST here */ - { request: { method: 'POST' }, response: { status: 400 } }, - - /* for GET requests, return a particular user */ - { - request: { method: 'GET' }, - response: function (ctx, id) { - ctx.body = users.find(user => user.id === Number(id)) - } - }, - - /* for PUT requests, update the record */ - { - request: { method: 'PUT' }, - response: function (ctx, id) { - const updatedUser = ctx.request.body - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1, updatedUser) - ctx.status = 200 - } - }, - - /* DELETE request: remove the record */ - { - request: { method: 'DELETE' }, - response: function (ctx, id) { - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1) - ctx.status = 200 - } - } -] - -module.exports = mockResponses -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/mock). - -### HTTPS Server - -Some modern techs (ServiceWorker, any `MediaDevices.getUserMedia()` request etc.) *must* be served from a secure origin (HTTPS). To launch an HTTPS server, supply a `--key` and `--cert` to local-web-server, for example: - -``` -$ ws --key localhost.key --cert localhost.crt -``` - -If you don't have a key and certificate it's trivial to create them. You do not need third-party verification (Verisign etc.) for development purposes. To get the green padlock in the browser, the certificate.. - -* must have a `Common Name` value matching the FQDN of the server -* must be verified by a Certificate Authority (but we can overrule this - see below) - -First create a certificate: - -1. Install openssl. - - `$ brew install openssl` - -2. Generate a RSA private key. - - `$ openssl genrsa -des3 -passout pass:x -out ws.pass.key 2048` - -3. Create RSA key. - - ``` - $ openssl rsa -passin pass:x -in ws.pass.key -out ws.key - ``` - -4. Create certificate request. The command below will ask a series of questions about the certificate owner. The most imporant answer to give is for `Common Name`, you can accept the default values for the others. **Important**: you **must** input your server's correct FQDN (`dev-server.local`, `laptop.home` etc.) into the `Common Name` field. The cert is only valid for the domain specified here. You can find out your computers host name by running the command `hostname`. For example, mine is `mba3.home`. - - `$ openssl req -new -key ws.key -out ws.csr` - -5. Generate self-signed certificate. - - `$ openssl x509 -req -days 365 -in ws.csr -signkey ws.key -out ws.crt` - -6. Clean up files we're finished with - - `$ rm ws.pass.key ws.csr` - -7. Launch HTTPS server. In iTerm, control-click the first URL (with the hostname matching `Common Name`) to launch your browser. - - ``` - $ ws --key ws.key --cert ws.crt - serving at https://mba3.home:8010, https://127.0.0.1:8010, https://192.168.1.203:8010 - ``` - -Chrome and Firefox will still complain your certificate has not been verified by a Certificate Authority. Firefox will offer you an `Add an exception` option, allowing you to ignore the warning and manually mark the certificate as trusted. In Chrome on Mac, you can manually trust the certificate another way: - -1. Open Keychain -2. Click File -> Import. Select the `.crt` file you created. -3. In the `Certificates` category, double-click the cert you imported. -4. In the `trust` section, underneath `when using this certificate`, select `Always Trust`. - -Now you have a valid, trusted certificate for development. - -#### Built-in certificate -As a quick win, you can run `ws` with the `https` flag. This will launch an HTTPS server using a [built-in certificate](https://github.com/75lb/local-web-server/tree/master/ssl) registered to the domain 127.0.0.1. - -### Stored config - -Use the same options every time? Persist then to `package.json`: -```json -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100, - "forbid": "*.json" - } -} -``` - -or `.local-web-server.json` -```json -{ - "port": 8100, - "forbid": "*.json" -} -``` - -local-web-server will merge and use all config found, searching from the current directory upward. In the case both `package.json` and `.local-web-server.json` config is found in the same directory, `.local-web-server.json` will take precedence. Options set on the command line take precedence over all. - -To inspect stored config, run: -```sh -$ ws --config -``` - -### Logging -By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log-format`: - -```sh -$ ws --log-format combined -serving at http://localhost:8000 -::1 - - [16/Nov/2015:11:16:52 +0000] "GET / HTTP/1.1" 200 12290 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2562.0 Safari/537.36" -``` - -The format value supplied is passed directly to [morgan](https://github.com/expressjs/morgan). The exception is `--log-format none` which disables all output. - -### Access Control - -By default, access to all files is allowed (including dot files). Use `--forbid` to establish a blacklist: -```sh -$ ws --forbid '*.json' '*.yml' -serving at http://localhost:8000 -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/forbid). - ### Other usage #### Debugging @@ -534,19 +131,6 @@ Disable etag response headers, forcing resources to be served in full every time $ ws --no-cache ``` -#### mime-types -You can set additional mime-type/extension mappings, or override the defaults by setting a `mime` value in the stored config. This value is passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine). Example: - -```json -{ - "mime": { - "text/plain": [ "php", "pl" ] - } -} -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/mime-override). - #### Log Visualisation Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md). diff --git a/lib/default-stack.js b/lib/default-stack.js new file mode 100644 index 0000000..df61838 --- /dev/null +++ b/lib/default-stack.js @@ -0,0 +1,291 @@ +'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('./middleware-stack') + +class DefaultStack extends MiddlewareStack { + /** + * 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 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) + } + }) + } + }) + 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 2852c2e..f8c4729 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -5,12 +5,20 @@ const path = require('path') const arrayify = require('array-back') const t = require('typical') const CommandLineTool = require('command-line-tool') -const MiddlewareStack = require('./middleware-stack') +const DefaultStack = require('./default-stack') const debug = require('./debug') +/** + * @module local-web-server + */ + const tool = new CommandLineTool() -class LocalWebServer extends MiddlewareStack { +/** + * @alias module:local-web-server + * @extends module:middleware-stack + */ +class LocalWebServer extends DefaultStack { _init (options) { this.options = this.options || Object.assign(options || {}, collectUserOptions(this.getOptionDefinitions())) } @@ -83,7 +91,7 @@ class LocalWebServer extends MiddlewareStack { } function onServerUp (port, directory, isHttps) { - const ipList = getIPList(isHttps) + const ipList = getIPList() .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${port}}`) .join(', ') @@ -94,7 +102,8 @@ function onServerUp (port, directory, isHttps) { )) } -function getIPList (isHttps) { + +function getIPList () { const flatten = require('reduce-flatten') const os = require('os') diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js index fda6a26..c8961e7 100644 --- a/lib/middleware-stack.js +++ b/lib/middleware-stack.js @@ -8,271 +8,21 @@ const t = require('typical') const compose = require('koa-compose') const flatten = require('reduce-flatten') +/** + * @module middleware-stack + */ + +/** + * @extends Array + * @alias module:middleware-stack + */ class MiddlewareStack extends Array { - add (middleware) { - this.push(middleware) - return this - } - /** - * allow from any origin + * @param {module:middleware-stack~middleware} + * @chainable */ - 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 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) - } - }) - } - }) - 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) - } - } - }) + add (middleware) { + this.push(middleware) return this } @@ -295,24 +45,14 @@ class MiddlewareStack extends Array { .filter(middleware => middleware) .reduce(flatten, []) .map(convert) - // console.error(require('util').inspect(middlewareStack, { depth: 3, colors: true })) return compose(middlewareStack) } } module.exports = MiddlewareStack -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 - } - }) -} +/** + * @typedef middleware + * @property optionDefinitions {object|object[]} + * @property middleware {function} + */ From 3ac7258a7253e96c223b67ab3ebcca0aed7eacb7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Jun 2016 22:43:09 +0100 Subject: [PATCH 021/136] docs --- README.md | 36 ---------- doc/api.md | 30 ++++++++ jsdoc2md/README.hbs | 193 ---------------------------------------------------- jsdoc2md/api.hbs | 8 +++ package.json | 2 +- 5 files changed, 39 insertions(+), 230 deletions(-) create mode 100644 doc/api.md delete mode 100644 jsdoc2md/README.hbs create mode 100644 jsdoc2md/api.hbs diff --git a/README.md b/README.md index 237b648..943f4e5 100644 --- a/README.md +++ b/README.md @@ -141,11 +141,6 @@ Ensure [node.js](http://nodejs.org) is installed first. Linux/Mac users may need $ npm install -g local-web-server ``` -This will install the `ws` tool globally. To see the available options, run: -```sh -$ ws --help -``` - ## Distribute with your project The standard convention with client-server applications is to add an `npm start` command to launch the server component. @@ -179,37 +174,6 @@ $ npm start serving at http://localhost:8100 ``` -## API Reference - - -* [local-web-server](#module_local-web-server) - * [LocalWebServer](#exp_module_local-web-server--LocalWebServer) ⇐ [middleware-stack](#module_middleware-stack) ⏏ - * _instance_ - * [.add(middleware)](#) ↩︎ - * _inner_ - * [~collectUserOptions()](#module_local-web-server--LocalWebServer..collectUserOptions) - - - -### LocalWebServer ⇐ [middleware-stack](#module_middleware-stack) ⏏ -**Kind**: Exported class -**Extends:** [middleware-stack](#module_middleware-stack) - - -#### localWebServer.add(middleware) ↩︎ -**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) -**Chainable** -**Params** - -- middleware [middleware](#module_middleware-stack--MiddlewareStack..middleware) - - - -#### LocalWebServer~collectUserOptions() -Return default, stored and command-line options combined - -**Kind**: inner method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - * * * © 2013-16 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..fe731a8 --- /dev/null +++ b/doc/api.md @@ -0,0 +1,30 @@ +## API Reference + + +* [local-web-server](#module_local-web-server) + * [LocalWebServer](#exp_module_local-web-server--LocalWebServer) ⇐ [middleware-stack](#module_middleware-stack) ⏏ + * _instance_ + * [.add(middleware)](#) ↩︎ + * _inner_ + * [~collectUserOptions()](#module_local-web-server--LocalWebServer..collectUserOptions) + + + +### LocalWebServer ⇐ [middleware-stack](#module_middleware-stack) ⏏ +**Kind**: Exported class +**Extends:** [middleware-stack](#module_middleware-stack) + + +#### localWebServer.add(middleware) ↩︎ +**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) +**Chainable** +**Params** + +- middleware [middleware](#module_middleware-stack--MiddlewareStack..middleware) + + + +#### LocalWebServer~collectUserOptions() +Return default, stored and command-line options combined + +**Kind**: inner method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) diff --git a/jsdoc2md/README.hbs b/jsdoc2md/README.hbs deleted file mode 100644 index 56d74d1..0000000 --- a/jsdoc2md/README.hbs +++ /dev/null @@ -1,193 +0,0 @@ -[![view on npm](http://img.shields.io/npm/v/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) -[![npm module downloads](http://img.shields.io/npm/dt/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) -[![Build Status](https://travis-ci.org/75lb/local-web-server.svg?branch=master)](https://travis-ci.org/75lb/local-web-server) -[![Dependency Status](https://david-dm.org/75lb/local-web-server.svg)](https://david-dm.org/75lb/local-web-server) -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) -[![Join the chat at https://gitter.im/75lb/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/75lb/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.*** - -# local-web-server -An application shell for building a simple, command-line web server for productive web development. - -It is trivial is bundle and deploy with your project. Also deploys to heroku well for demo projects. - -It comes with some middleware built-in, which you need not use but will get you up and running for the following use cases: - -* Static or Single Page Application front-end development where you have - * No backend, an existing remote API or need to mock-up an API. - -Application Shell - * HTTP or HTTPS server - * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.) - * Add your middleware - * Use any combination of built-in and custom middleware - * specify options (for command line or config) - * Accepts Koa v1 or 2 middleware - * Bundle with your front-end project - * Configuration is via json file or command-line (latter taking presedence) - * Outputs a dynamic statistics view to the terminal - - -Built-in Middleware (all optional) - * Rewrite routes to local or remote resources - * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) - * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md) - * Proxy server - * Map local routes to remote servers. Removes CORS pain when consuming remote services. - * Back-end service mocking - * Prototype a web service, microservice, REST API etc. - * Mocks are defined with config (static), or code (dynamic). - * CORS-friendly, all origins allowed by default. - -## Synopsis -local-web-server is a command-line tool. To serve the current directory, run `ws`. - -
    $ ws --help
    -
    -local-web-server
    -
    -  A simple web-server for productive front-end development.
    -
    -Synopsis
    -
    -  $ ws [<server options>]
    -  $ ws --config
    -  $ ws --help
    -
    -Server
    -
    -  -p, --port number              Web server port.
    -  -d, --directory path           Root directory, defaults to the current directory.
    -  -f, --log-format string        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').
    -  -r, --rewrite expression ...   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'.
    -  -s, --spa file                 Path to a Single Page App, e.g. app.html.
    -  -c, --compress                 Serve gzip-compressed resources, where applicable.
    -  -b, --forbid path ...          A list of forbidden routes.
    -  -n, --no-cache                 Disable etag-based caching -forces loading from disk each request.
    -  --key file                     SSL key. Supply along with --cert to launch a https server.
    -  --cert file                    SSL cert. Supply along with --key to launch a https server.
    -  --https                        Enable HTTPS using a built-in key and cert, registered to the
    -                                 domain 127.0.0.1.
    -  --verbose                      Verbose output, useful for debugging.
    -
    -Misc
    -
    -  -h, --help    Print these usage instructions.
    -  --config      Print the stored config.
    -
    -  Project home: https://github.com/75lb/local-web-server
    -
    - -## Examples - -For the examples below, we assume we're in a project directory looking like this: - -```sh -. -├── css -│   └── style.css -├── index.html -└── package.json -``` - -All paths/routes are specified using [express syntax](http://expressjs.com/guide/routing.html#route-paths). To run the example projects linked below, clone the project, move into the example directory specified, run `ws`. - -### Static site - -Fire up your static site on the default port: -```sh -$ ws -serving at http://localhost:8000 -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/simple). - -### Other usage - -#### Debugging - -Prints information about loaded middleware, arguments, remote proxy fetches etc. -```sh -$ ws --verbose -``` - -#### Compression - -Serve gzip-compressed resources, where applicable -```sh -$ ws --compress -``` - -#### Disable caching - -Disable etag response headers, forcing resources to be served in full every time. -```sh -$ ws --no-cache -``` - -#### Log Visualisation -Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md). - -## Install -Ensure [node.js](http://nodejs.org) is installed first. Linux/Mac users may need to run the following commands with `sudo`. - -```sh -$ npm install -g local-web-server -``` - -This will install the `ws` tool globally. To see the available options, run: -```sh -$ ws --help -``` - -## Distribute with your project -The standard convention with client-server applications is to add an `npm start` command to launch the server component. - -1\. Install the server as a dev dependency - -```sh -$ npm install local-web-server --save-dev -``` - -2\. Add a `start` command to your `package.json`: - -```json -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100, - "forbid": "*.json" - }, - "scripts": { - "start": "ws" - } -} -``` - -3\. Document how to build and launch your site - -```sh -$ npm install -$ npm start -serving at http://localhost:8100 -``` - -## API Reference - -{{#module name="local-web-server"}} -{{>body~}} -{{>member-index~}} -{{>separator~}} -{{>members~}} -{{/module}} - -* * * - -© 2013-16 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). diff --git a/jsdoc2md/api.hbs b/jsdoc2md/api.hbs new file mode 100644 index 0000000..ee9b32f --- /dev/null +++ b/jsdoc2md/api.hbs @@ -0,0 +1,8 @@ +## API Reference + +{{#module name="local-web-server"}} +{{>body~}} +{{>member-index~}} +{{>separator~}} +{{>members~}} +{{/module}} diff --git a/package.json b/package.json index f348e49..f5b971c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "scripts": { "test": "tape test/*/*.js", - "docs": "jsdoc2md -t jsdoc2md/README.hbs -p list lib/*.js > README.md; echo", + "docs": "jsdoc2md -t jsdoc2md/api.hbs -p list lib/*.js > doc/api.md; echo", "cover": "istanbul cover ./node_modules/.bin/tape test/*.js && cat coverage/lcov.info | coveralls && rm -rf coverage; echo" }, "repository": "https://github.com/75lb/local-web-server", From 6100ad6f6e0df29e0eec8deea51a13629a32b4b5 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Jun 2016 22:57:34 +0100 Subject: [PATCH 022/136] docs --- README.md | 98 ++++++++++++------------------------------------------- doc/distribute.md | 32 ++++++++++++++++++ 2 files changed, 52 insertions(+), 78 deletions(-) create mode 100644 doc/distribute.md diff --git a/README.md b/README.md index 943f4e5..c2053e3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Application Shell * specify options (for command line or config) * Accepts Koa v1 or 2 middleware * Bundle with your front-end project - * Configuration is via json file or command-line (latter taking presedence) + * Configuration is via json file or command-line (latter taking precedence) * Outputs a dynamic statistics view to the terminal @@ -51,30 +51,32 @@ local-web-server is a command-line tool. To serve the current directory, run `ws Synopsis - $ ws [<server options>] + $ ws [--verbose] [] [] $ ws --config $ ws --help Server - -p, --port number Web server port. - -d, --directory path Root directory, defaults to the current directory. - -f, --log-format string 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'). - -r, --rewrite expression ... 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'. - -s, --spa file Path to a Single Page App, e.g. app.html. - -c, --compress Serve gzip-compressed resources, where applicable. + -p, --port number Web server port. + --key file SSL key. Supply along with --cert to launch a https server. + --cert file SSL cert. Supply along with --key to launch a https server. + --https Enable HTTPS using a built-in key and cert, registered to the domain + 127.0.0.1. + +Middleware + + -r, --rewrite expression ... 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'. -b, --forbid path ... A list of forbidden routes. -n, --no-cache Disable etag-based caching -forces loading from disk each request. - --key file SSL key. Supply along with --cert to launch a https server. - --cert file SSL cert. Supply along with --key to launch a https server. - --https Enable HTTPS using a built-in key and cert, registered to the - domain 127.0.0.1. - --verbose Verbose output, useful for debugging. + -c, --compress Serve gzip-compressed resources, where applicable. + -f, --log-format string 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'). + -s, --spa file Path to a Single Page App, e.g. app.html. + -d, --directory path Root directory, defaults to the current directory. Misc @@ -108,72 +110,12 @@ serving at http://localhost:8000 [Example](https://github.com/75lb/local-web-server/tree/master/example/simple). -### Other usage - -#### Debugging - -Prints information about loaded middleware, arguments, remote proxy fetches etc. -```sh -$ ws --verbose -``` - -#### Compression - -Serve gzip-compressed resources, where applicable -```sh -$ ws --compress -``` - -#### Disable caching - -Disable etag response headers, forcing resources to be served in full every time. -```sh -$ ws --no-cache -``` - -#### Log Visualisation -Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md). - ## Install Ensure [node.js](http://nodejs.org) is installed first. Linux/Mac users may need to run the following commands with `sudo`. ```sh $ npm install -g local-web-server ``` - -## Distribute with your project -The standard convention with client-server applications is to add an `npm start` command to launch the server component. - -1\. Install the server as a dev dependency - -```sh -$ npm install local-web-server --save-dev -``` - -2\. Add a `start` command to your `package.json`: - -```json -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100, - "forbid": "*.json" - }, - "scripts": { - "start": "ws" - } -} -``` - -3\. Document how to build and launch your site - -```sh -$ npm install -$ npm start -serving at http://localhost:8100 -``` - * * * © 2013-16 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). diff --git a/doc/distribute.md b/doc/distribute.md new file mode 100644 index 0000000..08ca8d3 --- /dev/null +++ b/doc/distribute.md @@ -0,0 +1,32 @@ +## Distribute with your project +The standard convention with client-server applications is to add an `npm start` command to launch the server component. + +1\. Install the server as a dev dependency + +```sh +$ npm install local-web-server --save-dev +``` + +2\. Add a `start` command to your `package.json`: + +```json +{ + "name": "example", + "version": "1.0.0", + "local-web-server": { + "port": 8100, + "forbid": "*.json" + }, + "scripts": { + "start": "ws" + } +} +``` + +3\. Document how to build and launch your site + +```sh +$ npm install +$ npm start +serving at http://localhost:8100 +``` From fec62b725337b7f26d87366ae026de4d57a005ce Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 22 Jun 2016 22:24:11 +0100 Subject: [PATCH 023/136] extract koa-mock-response and lws stack --- bin/cli.js | 15 +------------ lib/default-stack.js | 24 +++++++++++++++++--- lib/local-web-server.js | 13 ++++++++--- lib/middleware-stack.js | 58 ------------------------------------------------- lib/middleware.js | 43 ------------------------------------ package.json | 7 +++++- 6 files changed, 38 insertions(+), 122 deletions(-) delete mode 100644 lib/middleware-stack.js diff --git a/bin/cli.js b/bin/cli.js index d4fbb15..fd6e338 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -3,17 +3,4 @@ const LocalWebServer = require('../') const ws = new LocalWebServer() -ws.addCors() - .addJson() - .addRewrite() - .addBodyParser() - .addBlacklist() - .addCache() - .addMimeOverride() - .addCompression() - .addLogging() - .addMockResponses() - .addSpa() - .addStatic() - .addIndex() - .listen() +ws.listen() diff --git a/lib/default-stack.js b/lib/default-stack.js index df61838..ebbf56f 100644 --- a/lib/default-stack.js +++ b/lib/default-stack.js @@ -7,9 +7,27 @@ const mw = require('./middleware') const t = require('typical') const compose = require('koa-compose') const flatten = require('reduce-flatten') -const MiddlewareStack = require('./middleware-stack') +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 */ @@ -194,13 +212,13 @@ class DefaultStack extends MiddlewareStack { } if (mock.responses) { - return mw.mockResponses(mock.route, mock.responses) + return mockResponses(mock.route, mock.responses) } else if (mock.response) { mock.target = { request: mock.request, response: mock.response } - return mw.mockResponses(mock.route, mock.target) + return mockResponses(mock.route, mock.target) } }) } diff --git a/lib/local-web-server.js b/lib/local-web-server.js index f8c4729..792dc65 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -18,15 +18,22 @@ const tool = new CommandLineTool() * @alias module:local-web-server * @extends module:middleware-stack */ -class LocalWebServer extends DefaultStack { +class LocalWebServer { + constructor (stack) { + this.stack = stack || new DefaultStack() + this.stack.addAll() + } _init (options) { - this.options = this.options || Object.assign(options || {}, collectUserOptions(this.getOptionDefinitions())) + 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.compose(this.options)) + app.use(this.stack.compose(this.options)) return app } diff --git a/lib/middleware-stack.js b/lib/middleware-stack.js deleted file mode 100644 index c8961e7..0000000 --- a/lib/middleware-stack.js +++ /dev/null @@ -1,58 +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') - -/** - * @module middleware-stack - */ - -/** - * @extends Array - * @alias module:middleware-stack - */ -class MiddlewareStack extends Array { - /** - * @param {module:middleware-stack~middleware} - * @chainable - */ - add (middleware) { - this.push(middleware) - return this - } - - getOptionDefinitions () { - return this - .filter(mw => mw.optionDefinitions) - .map(mw => mw.optionDefinitions) - .reduce(flatten, []) - .map(def => { - def.group = 'middleware' - return def - }) - } - compose (options) { - const convert = require('koa-convert') - const middlewareStack = this - .filter(mw => mw.middleware) - .map(mw => mw.middleware) - .map(middleware => middleware(options)) - .filter(middleware => middleware) - .reduce(flatten, []) - .map(convert) - return compose(middlewareStack) - } -} - -module.exports = MiddlewareStack - -/** - * @typedef middleware - * @property optionDefinitions {object|object[]} - * @property middleware {function} - */ diff --git a/lib/middleware.js b/lib/middleware.js index 964b2b2..23ce959 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -10,7 +10,6 @@ const debug = require('./debug') * @module middleware */ exports.proxyRequest = proxyRequest -exports.mockResponses = mockResponses exports.mime = mime function proxyRequest (route) { @@ -58,45 +57,3 @@ function mime (mimeTypes) { }) } } - -function mockResponses (route, targets) { - targets = arrayify(targets) - const pathRe = pathToRegexp(route) - debug('mock route: %s, targets: %s', route, targets.length) - - return function mockResponse (ctx, next) { - if (pathRe.test(ctx.path)) { - const testValue = require('test-value') - - /* find a mock with compatible method and accepts */ - let target = targets.find(target => { - return testValue(target, { - request: { - method: [ ctx.method, undefined ], - accepts: type => ctx.accepts(type) - } - }) - }) - - /* else take the first target without a request (no request means 'all requests') */ - if (!target) { - target = targets.find(target => !target.request) - } - - debug(`mock path: ${ctx.path} target: ${target && target.name || 'unnamed'}`) - - if (target) { - if (t.isFunction(target.response)) { - const pathMatches = ctx.path.match(pathRe).slice(1) - return target.response.apply(null, [ctx].concat(pathMatches)) - } else if (t.isPlainObject(target.response)) { - Object.assign(ctx.response, target.response) - } else { - throw new Error(`Invalid response: ${JSON.stringify(target.response)}`) - } - } - } else { - return next() - } - } -} diff --git a/package.json b/package.json index f5b971c..b711b35 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,10 @@ "development", "cors", "mime", - "rest" + "rest", + "mock", + "api", + "proxy" ], "engines": { "node": ">=4.0.0" @@ -43,12 +46,14 @@ "koa-convert": "^1.2.0", "koa-etag": "^2.1.1", "koa-json": "^1.1.3", + "koa-mock-response": "0.0.2", "koa-morgan": "^1.0.1", "koa-rewrite": "^2.1.0", "koa-route": "^3.0.0", "koa-send": "^3.2.0", "koa-serve-index": "^1.1.1", "koa-static": "^2.0.0", + "local-web-server-stack": "github:75lb/local-web-server-stack", "path-to-regexp": "^1.5.0", "reduce-flatten": "^1.0.0", "stream-log-stats": "^1.1.3", From 6440486b0c5d63b12c314239b96c06e7c006cc4f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 24 Jun 2016 21:18:23 +0100 Subject: [PATCH 024/136] added --stack option.. refactor --- example/stack/cache-control.js | 23 +++ lib/cli-data.js | 4 + lib/debug.js | 12 -- lib/default-stack.js | 309 ----------------------------------------- lib/local-web-server.js | 109 ++++++++++----- lib/middleware.js | 59 -------- package.json | 3 +- test/fixture/ajax.html | 18 --- test/fixture/file.txt | 1 - test/fixture/one/file.txt | 1 - test/fixture/spa/one.txt | 1 - test/fixture/spa/two.txt | 1 - 12 files changed, 105 insertions(+), 436 deletions(-) create mode 100644 example/stack/cache-control.js delete mode 100644 lib/debug.js delete mode 100644 lib/default-stack.js delete mode 100644 lib/middleware.js delete mode 100644 test/fixture/ajax.html delete mode 100644 test/fixture/file.txt delete mode 100644 test/fixture/one/file.txt delete mode 100644 test/fixture/spa/one.txt delete mode 100644 test/fixture/spa/two.txt 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
    
    From 99f5b19d27328ea9cc09fe7c7196e82a711ba602 Mon Sep 17 00:00:00 2001
    From: Lloyd Brookes 
    Date: Sat, 25 Jun 2016 15:24:30 +0100
    Subject: [PATCH 025/136] update stack examples and deps
    
    ---
     .../custom/cache-control/.local-web-server.json    |  3 ---
     example/custom/cache-control/server.js             | 18 -------------
     example/custom/live-reload-optional/server.js      | 19 -------------
     example/custom/live-reload/server.js               |  9 -------
     example/stack/cache-control.js                     | 24 ++++++++++-------
     .../live-reload-optional}/index.html               |  2 +-
     example/stack/live-reload-optional/stack.js        | 24 +++++++++++++++++
     .../live-reload}/index.html                        |  0
     example/stack/live-reload/stack.js                 | 14 ++++++++++
     lib/local-web-server.js                            |  3 +--
     package.json                                       | 31 +++++-----------------
     11 files changed, 60 insertions(+), 87 deletions(-)
     delete mode 100644 example/custom/cache-control/.local-web-server.json
     delete mode 100644 example/custom/cache-control/server.js
     delete mode 100644 example/custom/live-reload-optional/server.js
     delete mode 100644 example/custom/live-reload/server.js
     rename example/{custom/live-reload => stack/live-reload-optional}/index.html (73%)
     create mode 100644 example/stack/live-reload-optional/stack.js
     rename example/{custom/live-reload-optional => stack/live-reload}/index.html (100%)
     create mode 100644 example/stack/live-reload/stack.js
    
    diff --git a/example/custom/cache-control/.local-web-server.json b/example/custom/cache-control/.local-web-server.json
    deleted file mode 100644
    index 89cb0a2..0000000
    --- a/example/custom/cache-control/.local-web-server.json
    +++ /dev/null
    @@ -1,3 +0,0 @@
    -{
    -  "maxage": 2000
    -}
    diff --git a/example/custom/cache-control/server.js b/example/custom/cache-control/server.js
    deleted file mode 100644
    index f72e532..0000000
    --- a/example/custom/cache-control/server.js
    +++ /dev/null
    @@ -1,18 +0,0 @@
    -'use strict'
    -const LocalWebServer = require('../../')
    -const cacheControl = require('koa-cache-control')
    -
    -const ws = new LocalWebServer()
    -ws.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()
    -  .listen()
    diff --git a/example/custom/live-reload-optional/server.js b/example/custom/live-reload-optional/server.js
    deleted file mode 100644
    index 7c48b99..0000000
    --- a/example/custom/live-reload-optional/server.js
    +++ /dev/null
    @@ -1,19 +0,0 @@
    -'use strict'
    -const Cli = require('../../')
    -const liveReload = require('koa-livereload')
    -
    -const ws = new Cli()
    -ws.addLogging('dev')
    -  .add({
    -    optionDefinitions: {
    -      name: 'live-reload', type: Boolean,
    -      description: 'Add live reload.'
    -    },
    -    middleware: function (options) {
    -      if (options['live-reload']) {
    -        return liveReload()
    -      }
    -    }
    -  })
    -  .addStatic()
    -  .listen()
    diff --git a/example/custom/live-reload/server.js b/example/custom/live-reload/server.js
    deleted file mode 100644
    index 9f2a00e..0000000
    --- a/example/custom/live-reload/server.js
    +++ /dev/null
    @@ -1,9 +0,0 @@
    -'use strict'
    -const Cli = require('../../')
    -const liveReload = require('koa-livereload')
    -
    -const ws = new Cli()
    -ws.addLogging('dev')
    -  .add({ middleware: liveReload })
    -  .addStatic()
    -  .listen()
    diff --git a/example/stack/cache-control.js b/example/stack/cache-control.js
    index 52863aa..074a52e 100644
    --- a/example/stack/cache-control.js
    +++ b/example/stack/cache-control.js
    @@ -5,19 +5,23 @@ 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 })
    -        }
    -      })
    +    return this.addLogging('dev')
    +      .addCacheControl()
           .addStatic()
           .addIndex()
       }
    +  addCacheControl () {
    +    this.add({
    +      optionDefinitions: {
    +        name: 'maxage', type: Number,
    +        description: 'The maxage to set on each response.'
    +      },
    +      middleware: function (options) {
    +        return cacheControl({ maxAge: options.maxage })
    +      }
    +    })
    +    return this
    +  }
     }
     
     module.exports = CacheControl
    diff --git a/example/custom/live-reload/index.html b/example/stack/live-reload-optional/index.html
    similarity index 73%
    rename from example/custom/live-reload/index.html
    rename to example/stack/live-reload-optional/index.html
    index 0cc6591..f467990 100644
    --- a/example/custom/live-reload/index.html
    +++ b/example/stack/live-reload-optional/index.html
    @@ -5,6 +5,6 @@
         live-reload demo
       
       
    -    

    Live reloaded attached

    +

    Live reloaded potentially attached

    diff --git a/example/stack/live-reload-optional/stack.js b/example/stack/live-reload-optional/stack.js new file mode 100644 index 0000000..aa4f4ce --- /dev/null +++ b/example/stack/live-reload-optional/stack.js @@ -0,0 +1,24 @@ +'use strict' +const LocalWebServer = require('../../../') +const liveReload = require('koa-livereload') +const DefaultStack = require('local-web-server-default-stack') + +class LiveReloadStack extends DefaultStack { + addAll () { + return this.addLogging('dev') + .add({ + optionDefinitions: { + name: 'live-reload', type: Boolean, + description: 'Add live reload.' + }, + middleware: function (options) { + if (options['live-reload']) { + return liveReload() + } + } + }) + .addStatic() + } +} + +module.exports = LiveReloadStack diff --git a/example/custom/live-reload-optional/index.html b/example/stack/live-reload/index.html similarity index 100% rename from example/custom/live-reload-optional/index.html rename to example/stack/live-reload/index.html diff --git a/example/stack/live-reload/stack.js b/example/stack/live-reload/stack.js new file mode 100644 index 0000000..217a428 --- /dev/null +++ b/example/stack/live-reload/stack.js @@ -0,0 +1,14 @@ +'use strict' +const LocalWebServer = require('../../../') +const liveReload = require('koa-livereload') +const DefaultStack = require('local-web-server-default-stack') + +class LiveReloadStack extends DefaultStack { + addAll () { + return this.addLogging('dev') + .add({ middleware: liveReload }) + .addStatic() + } +} + +module.exports = LiveReloadStack diff --git a/lib/local-web-server.js b/lib/local-web-server.js index dd84681..1857cbe 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -5,7 +5,6 @@ const path = require('path') const arrayify = require('array-back') const t = require('typical') const CommandLineTool = require('command-line-tool') -const DefaultStack = require('local-web-server-default-stack') /** * @module local-web-server @@ -30,7 +29,7 @@ class LocalWebServer { if (/^-/.test(stackPath)) stackPath = null } - const stackModule = loadStack(stackPath) || DefaultStack + const stackModule = loadStack(stackPath) || require('local-web-server-default-stack') this.stack = new stackModule() this.stack.addAll() const middlewareOptionDefinitions = this.stack.getOptionDefinitions() diff --git a/package.json b/package.json index ff5a604..f097539 100644 --- a/package.json +++ b/package.json @@ -34,38 +34,19 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", - "command-line-tool": "~0.3.0", - "config-master": "^2.0.2", - "http-proxy": "^1.13.3", - "kcors": "^1.2.1", + "command-line-tool": "~0.3.1", + "config-master": "^2.0.3", "koa": "^2.0.0", - "koa-bodyparser": "^3.0.0", - "koa-compose": "^3.1.0", - "koa-compress": "^1.0.9", - "koa-conditional-get": "^1.0.3", - "koa-convert": "^1.2.0", - "koa-etag": "^2.1.1", - "koa-json": "^1.1.3", - "koa-mock-response": "0.0.2", - "koa-morgan": "^1.0.1", - "koa-rewrite": "^2.1.0", - "koa-route": "^3.0.0", - "koa-send": "^3.2.0", - "koa-serve-index": "^1.1.1", - "koa-static": "^2.0.0", - "local-web-server-stack": "github:75lb/local-web-server-stack", - "path-to-regexp": "^1.5.0", - "reduce-flatten": "^1.0.0", - "stream-log-stats": "^1.1.3", - "test-value": "^2.0.0", + "local-web-server-default-stack": "github:local-web-server/default-stack", + "reduce-flatten": "^1.0.1", "typical": "^2.4.2", "walk-back": "^2.0.1" }, "devDependencies": { "jsdoc-to-markdown": "^1.3.6", "koa-cache-control": "^1.0.0", - "koa-livereload": "^0.1.23", + "koa-livereload": "~0.2.0", "req-then": "~0.2.4", - "tape": "^4.5.1" + "tape": "^4.6.0" } } From 84443a7ac5151d52cdb4de9afd181b8c4de1b63c Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 27 Jun 2016 00:03:11 +0100 Subject: [PATCH 026/136] refactor --- lib/local-web-server.js | 80 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 1857cbe..72c6e48 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -5,6 +5,7 @@ const path = require('path') const arrayify = require('array-back') const t = require('typical') const CommandLineTool = require('command-line-tool') +const flatten = require('reduce-flatten') /** * @module local-web-server @@ -22,18 +23,37 @@ class LocalWebServer { const commandLineUsage = require('command-line-usage') const cli = require('../lib/cli-data') - let stackPath + /* manually scan for any --stack passed, as we may need to display stack options */ + const stackPaths = [] const stackIndex = process.argv.indexOf('--stack') if (stackIndex > -1) { - stackPath = process.argv[stackIndex + 1] - if (/^-/.test(stackPath)) stackPath = null + for (var i = stackIndex + 1; i < process.argv.length; i++) { + const stackPath = process.argv[i] + if (/^-/.test(stackPath)) { + break + } else { + stackPaths.push(stackPath) + } + } } - const stackModule = loadStack(stackPath) || require('local-web-server-default-stack') - this.stack = new stackModule() - this.stack.addAll() - const middlewareOptionDefinitions = this.stack.getOptionDefinitions() - // console.log(middlewareOptionDefinitions) + /* load the stack */ + // if (!stackPaths.length) stackPaths.push('local-web-server-default-stack') + // console.log(stackPaths) + const stackModules = stackPaths + .map(stackPath => loadStack(stackPath)) + .map(Middleware => new Middleware()) + + /* gather stack option definitions and parse the command line */ + const middlewareOptionDefinitions = stackModules + .filter(mw => mw.optionDefinitions) + .map(mw => mw.optionDefinitions()) + .reduce(flatten, []) + .map(def => { + def.group = 'middleware' + return def + }) + const usage = commandLineUsage(cli.usage(middlewareOptionDefinitions)) let options = {} @@ -44,31 +64,48 @@ class LocalWebServer { tool.halt(err) } + /* combine in stored config */ 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 + // console.log(options) + if (options.verbose) { // debug.setLevel(1) } + + /* --config */ if (options.config) { tool.stop(JSON.stringify(options, null, ' '), 0) + + /* --version */ } else if (options.version) { const pkg = require(path.resolve(__dirname, '..', 'package.json')) tool.stop(pkg.version) + + /* --help */ + } else if (options.help) { + tool.stop(usage) } else { - if (this.options.help) { - tool.stop(usage) - } + const compose = require('koa-compose') + const convert = require('koa-convert') + const middlewareStack = stackModules + .filter(mw => mw.middleware) + .map(mw => mw.middleware) + .map(middleware => middleware(options)) + .filter(middleware => middleware) + .reduce(flatten, []) + .map(convert) + this.stack = compose(middlewareStack) } } getApplication (options) { const Koa = require('koa') const app = new Koa() - app.use(this.stack.compose(this.options)) + app.use(this.stack) return app } @@ -163,23 +200,32 @@ function collectUserOptions (mwOptionDefinitions) { return options } +/** + * Loads a module by either path or name. + * @returns {object} + */ 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) + const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath)) if (foundPath) { module = require(foundPath) + } else { + const foundPath2 = walkBack(process.cwd(), path.join('node_modules', modulePath)) + if (foundPath2) { + module = require(foundPath2) + } } } } + if (!module.prototype.middleware) { + tool.halt(new Error('Must supply a Middleware')) + } return module } From 3d521399f600a73a1c97067dd7a785a361fee209 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 29 Jun 2016 22:40:34 +0100 Subject: [PATCH 027/136] refactor, tests --- README.md | 2 +- lib/cli-data.js | 2 +- lib/local-web-server.js | 79 +++++++++++++++++++++++++++++++------------------ package.json | 1 + test/test-middleware.js | 12 ++++++++ test/test.js | 36 ++++++++++++++++++++++ 6 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 test/test-middleware.js create mode 100644 test/test.js diff --git a/README.md b/README.md index c2053e3..779a6e6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.*** # local-web-server -An application shell for building a simple, command-line web server for productive web development. +An application shell for building a simple, command-line web server for productive web development. It contains no middleware of its own but will load default-stack unless you specify otherwise. It is trivial is bundle and deploy with your project. Also deploys to heroku well for demo projects. diff --git a/lib/cli-data.js b/lib/cli-data.js index b1d4eb2..716326b 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -4,7 +4,7 @@ exports.optionDefinitions = [ description: 'Web server port.', group: 'server' }, { - name: 'stack', type: String, + name: 'stack', type: String, multiple: true, description: 'Middleware stack.', group: 'server' }, { diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 72c6e48..dabf476 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -18,13 +18,14 @@ const tool = new CommandLineTool() * @extends module:middleware-stack */ class LocalWebServer { - constructor (stack) { + constructor (initOptions) { + initOptions = initOptions || {} const commandLineArgs = require('command-line-args') const commandLineUsage = require('command-line-usage') const cli = require('../lib/cli-data') /* manually scan for any --stack passed, as we may need to display stack options */ - const stackPaths = [] + const stackPaths = initOptions.stack || [] const stackIndex = process.argv.indexOf('--stack') if (stackIndex > -1) { for (var i = stackIndex + 1; i < process.argv.length; i++) { @@ -38,11 +39,30 @@ class LocalWebServer { } /* load the stack */ - // if (!stackPaths.length) stackPaths.push('local-web-server-default-stack') - // console.log(stackPaths) + if (!stackPaths.length) stackPaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) const stackModules = stackPaths .map(stackPath => loadStack(stackPath)) .map(Middleware => new Middleware()) + .map(module => { + if (module.stack) { + const featureStack = module.stack() + module.optionDefinitions = function () { + return featureStack + .map(Feature => new Feature()) + .map(feature => feature.optionDefinitions && feature.optionDefinitions()) + .filter(definitions => definitions) + .reduce(flatten, []) + } + module.middleware = function (options) { + return featureStack + .map(Feature => new Feature()) + .map(feature => feature.middleware(options)) + .reduce(flatten, []) + .filter(mw => mw) + } + } + return module + }) /* gather stack option definitions and parse the command line */ const middlewareOptionDefinitions = stackModules @@ -67,11 +87,9 @@ class LocalWebServer { /* combine in stored config */ const loadConfig = require('config-master') const stored = loadConfig('local-web-server') - options = Object.assign(stored, options.server, options.middleware, options.misc) + options = Object.assign({ port: 8000 }, initOptions, stored, options.server, options.middleware, options.misc) this.options = options - // console.log(options) - if (options.verbose) { // debug.setLevel(1) } @@ -95,33 +113,32 @@ class LocalWebServer { .filter(mw => mw.middleware) .map(mw => mw.middleware) .map(middleware => middleware(options)) - .filter(middleware => middleware) .reduce(flatten, []) + .filter(middleware => middleware) .map(convert) this.stack = compose(middlewareStack) } } - getApplication (options) { + getApplication () { const Koa = require('koa') const app = new Koa() app.use(this.stack) + app.on('error', err => { + if (this.options['log-format']) { + console.error(ansi.format(err.stack, 'red')) + } + }) return app } - getServer (options) { - const app = this.getApplication(options) - options = this.options + getServer () { + const app = this.getApplication() + const options = this.options let key = options.key let cert = options.cert - app.on('error', err => { - if (options['log-format']) { - console.error(ansi.format(err.stack, 'red')) - } - }) - - if (options.https) { + if (options.https && !(key && cert)) { key = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.key') cert = path.resolve(__dirname, '..', 'ssl', '127.0.0.1.crt') } @@ -144,15 +161,19 @@ class LocalWebServer { return server } - listen (options, callback) { - options = this.options - const server = this.getServer() - const port = options.port || 8000 - server.listen(port, () => { - onServerUp(port, options.directory, server.isHttps) - if (callback) callback() + listen () { + const options = this.options + const server = this._server = this.getServer() + return new Promise ((resolve, reject) => { + server.listen(options.port, () => { + onServerUp(options.port, options.directory, server.isHttps) + resolve(server) + }) }) - return server + } + + close () { + this._server.close() } } @@ -223,8 +244,8 @@ function loadStack (modulePath) { } } } - if (!module.prototype.middleware) { - tool.halt(new Error('Must supply a Middleware')) + if (!(module && (module.prototype.middleware || module.prototype.stack))) { + tool.halt(new Error('Not valid Middleware: ' + modulePath)) } return module } diff --git a/package.json b/package.json index f097539..1439c92 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "jsdoc-to-markdown": "^1.3.6", "koa-cache-control": "^1.0.0", "koa-livereload": "~0.2.0", + "node-fetch": "^1.5.3", "req-then": "~0.2.4", "tape": "^4.6.0" } diff --git a/test/test-middleware.js b/test/test-middleware.js new file mode 100644 index 0000000..137438c --- /dev/null +++ b/test/test-middleware.js @@ -0,0 +1,12 @@ +'use strict' + +class TestMiddleware { + middleware (option) { + return function (ctx, next) { + ctx.body = '1234512345' + return next() + } + } +} + +module.exports = TestMiddleware diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..a8a7a7c --- /dev/null +++ b/test/test.js @@ -0,0 +1,36 @@ +'use strict' +const test = require('tape') +const request = require('req-then') +const LocalWebServer = require('../') +const c = require('./common') +const path = require('path') + +test('stack', function (t) { + t.plan(2) + const ws = new LocalWebServer({ + stack: [ path.resolve(__dirname, 'test-middleware.js') ] + }) + const server = ws.getServer() + server.listen(8100, () => { + request('http://localhost:8100/') + .then(c.checkResponse(t, 200, /1234512345/)) + .then(server.close.bind(server)) + .catch(c.fail(t)) + }) +}) + +test('https', function (t) { + t.plan(2) + const ws = new LocalWebServer({ + stack: [ path.resolve(__dirname, 'test-middleware.js') ], + https: true, + port: 8100 + }) + ws.listen() + .then(() => { + request('https://localhost:8100/') + .then(c.checkResponse(t, 200, /1234512345/)) + .then(ws.close.bind(ws)) + .catch(c.fail(t)) + }) +}) From 3e54a492f7d6515d52f386e2a115c14f297247e6 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 1 Jul 2016 20:53:10 +0100 Subject: [PATCH 028/136] log.format, error reporting --- README.md | 2 +- doc/logging.md | 8 ++++---- doc/visualisation.md | 2 +- lib/local-web-server.js | 27 +++++++++++++++++++++------ package.json | 2 +- test/compress/big-file.txt | 8 ++++---- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 779a6e6..f2b379f 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ local-web-server is a command-line tool. To serve the current directory, run `ws -b, --forbid path ... A list of forbidden routes. -n, --no-cache Disable etag-based caching -forces loading from disk each request. -c, --compress Serve gzip-compressed resources, where applicable. - -f, --log-format string If a format is supplied an access log is written to stdout. If not, a dynamic + -f, --log.format string 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'). diff --git a/doc/logging.md b/doc/logging.md index c41cd21..593c0ca 100644 --- a/doc/logging.md +++ b/doc/logging.md @@ -1,13 +1,13 @@ # Logging -By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log-format`: +By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log.format`: ```sh -$ ws --log-format combined +$ ws --log.format combined serving at http://localhost:8000 ::1 - - [16/Nov/2015:11:16:52 +0000] "GET / HTTP/1.1" 200 12290 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2562.0 Safari/537.36" ``` -The format value supplied is passed directly to [morgan](https://github.com/expressjs/morgan). The exception is `--log-format none` which disables all output. +The format value supplied is passed directly to [morgan](https://github.com/expressjs/morgan). The exception is `--log.format none` which disables all output. # Visualisation @@ -17,7 +17,7 @@ To get live statistics in [goaccess](http://goaccess.io/), first create this con ``` time-format %T date-format %d/%b/%Y -log-format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" +log.format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" ``` Then, start the server, outputting `combined` format logs to disk: diff --git a/doc/visualisation.md b/doc/visualisation.md index f143719..c621023 100644 --- a/doc/visualisation.md +++ b/doc/visualisation.md @@ -4,7 +4,7 @@ To get live statistics in [goaccess](http://goaccess.io/), first create this con ``` time-format %T date-format %d/%b/%Y -log-format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" +log.format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" ``` Then, start the server, outputting `combined` format logs to disk: diff --git a/lib/local-web-server.js b/lib/local-web-server.js index dabf476..a61b5f9 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -77,11 +77,16 @@ class LocalWebServer { const usage = commandLineUsage(cli.usage(middlewareOptionDefinitions)) let options = {} + const allOptionDefinitions = cli.optionDefinitions.concat(middlewareOptionDefinitions) try { - options = commandLineArgs(cli.optionDefinitions.concat(middlewareOptionDefinitions)) + options = commandLineArgs(allOptionDefinitions) } catch (err) { + tool.printError(err) + tool.printError(allOptionDefinitions.map(def => { + return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` + }).join('\n')) console.error(usage) - tool.halt(err) + tool.halt() } /* combine in stored config */ @@ -125,7 +130,7 @@ class LocalWebServer { const app = new Koa() app.use(this.stack) app.on('error', err => { - if (this.options['log-format']) { + if (this.options['log.format']) { console.error(ansi.format(err.stack, 'red')) } }) @@ -189,7 +194,6 @@ function onServerUp (port, directory, isHttps) { )) } - function getIPList () { const flatten = require('reduce-flatten') const os = require('os') @@ -227,25 +231,36 @@ function collectUserOptions (mwOptionDefinitions) { */ function loadStack (modulePath) { let module + const tried = [] if (modulePath) { const fs = require('fs') try { + tried.push(path.resolve(modulePath)) module = require(path.resolve(modulePath)) } catch (err) { const walkBack = require('walk-back') const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath)) + tried.push('local-web-server-' + modulePath) if (foundPath) { module = require(foundPath) } else { const foundPath2 = walkBack(process.cwd(), path.join('node_modules', modulePath)) + tried.push(modulePath) if (foundPath2) { module = require(foundPath2) } } } } - if (!(module && (module.prototype.middleware || module.prototype.stack))) { - tool.halt(new Error('Not valid Middleware: ' + modulePath)) + if (module) { + if (!(module.prototype.middleware || module.prototype.stack)) { + const insp = require('util').inspect(module, { depth: 3, colors: true }) + const msg = `Not valid Middleware at: ${insp}` + tool.halt(new Error(msg)) + } + } else { + const msg = `No module found at: \n${tried.join('\n')}` + tool.halt(new Error(msg)) } return module } diff --git a/package.json b/package.json index 1439c92..e47ebf2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", - "command-line-tool": "~0.3.1", + "command-line-tool": "75lb/command-line-tool", "config-master": "^2.0.3", "koa": "^2.0.0", "local-web-server-default-stack": "github:local-web-server/default-stack", diff --git a/test/compress/big-file.txt b/test/compress/big-file.txt index d5b9685..e837549 100644 --- a/test/compress/big-file.txt +++ b/test/compress/big-file.txt @@ -54,7 +54,7 @@ $ ws --help Server -p, --port Web server port --f, --log-format If a format is supplied an access log is written to stdout. If not, a statistics view is displayed. Use a +-f, --log.format If a format is supplied an access log is written to stdout. If not, a statistics view is displayed. Use a preset ('none', 'dev','combined', 'short', 'tiny' or 'logstalgia') or supply a custom format (e.g. ':method -> :url'). -d, --directory Root directory, defaults to the current directory @@ -90,11 +90,11 @@ $ ws --compress ``` ### Logging -Passing a value to `--log-format` will write an access log to `stdout`. +Passing a value to `--log.format` will write an access log to `stdout`. Either use a built-in [morgan](https://github.com/expressjs/morgan) logger preset: ```sh -$ ws --log-format short +$ ws --log.format short ``` Or a custom [morgan](https://github.com/expressjs/morgan) log format: @@ -123,7 +123,7 @@ Or in a `.local-web-server.json` file stored in the directory you want to serve ```json { "port": 8100, - "log-format": "tiny" + "log.format": "tiny" } ``` From 7910a424c298b87327f3b51f401c8fc3a27135fe Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 5 Jul 2016 20:38:29 +0100 Subject: [PATCH 029/136] refactor, linting --- bin/cli.js | 1 + lib/local-web-server.js | 55 +++++++++++++++++-------------------------------- package.json | 1 - 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index fd6e338..54493ed 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -4,3 +4,4 @@ const LocalWebServer = require('../') const ws = new LocalWebServer() ws.listen() + .catch(err => console.error(err.stack)) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index a61b5f9..802d0b0 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -2,8 +2,6 @@ 'use strict' const ansi = require('ansi-escape-sequences') const path = require('path') -const arrayify = require('array-back') -const t = require('typical') const CommandLineTool = require('command-line-tool') const flatten = require('reduce-flatten') @@ -69,6 +67,7 @@ class LocalWebServer { .filter(mw => mw.optionDefinitions) .map(mw => mw.optionDefinitions()) .reduce(flatten, []) + .filter(def => def) .map(def => { def.group = 'middleware' return def @@ -112,25 +111,29 @@ class LocalWebServer { } else if (options.help) { tool.stop(usage) } else { - const compose = require('koa-compose') - const convert = require('koa-convert') - const middlewareStack = stackModules - .filter(mw => mw.middleware) - .map(mw => mw.middleware) - .map(middleware => middleware(options)) - .reduce(flatten, []) - .filter(middleware => middleware) - .map(convert) - this.stack = compose(middlewareStack) + this.stack = stackModules } } getApplication () { const Koa = require('koa') const app = new Koa() - app.use(this.stack) + const compose = require('koa-compose') + const convert = require('koa-convert') + + const middlewareStack = this.stack + .filter(mw => mw.middleware) + .map(mw => mw.middleware(this.options)) + .reduce(flatten, []) + .filter(mw => mw) + .map(convert) + + app.use(compose(middlewareStack)) app.on('error', err => { - if (this.options['log.format']) { + const defaultLogInUse = this.stack.some(mw => mw.constructor.name === 'Log') + if (defaultLogInUse) { + if (this.options['log.format']) console.error(ansi.format(err.stack, 'red')) + } else { console.error(ansi.format(err.stack, 'red')) } }) @@ -169,9 +172,9 @@ class LocalWebServer { listen () { const options = this.options const server = this._server = this.getServer() - return new Promise ((resolve, reject) => { + return new Promise((resolve, reject) => { server.listen(options.port, () => { - onServerUp(options.port, options.directory, server.isHttps) + onServerUp(options.port, options['static.root'], server.isHttps) resolve(server) }) }) @@ -207,25 +210,6 @@ function getIPList () { } /** - * Return default, stored and command-line options combined - */ -function collectUserOptions (mwOptionDefinitions) { - const loadConfig = require('config-master') - const stored = loadConfig('local-web-server') - const cli = require('../lib/cli-data') - - /* parse command line args */ - 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) - return options -} - -/** * Loads a module by either path or name. * @returns {object} */ @@ -233,7 +217,6 @@ function loadStack (modulePath) { let module const tried = [] if (modulePath) { - const fs = require('fs') try { tried.push(path.resolve(modulePath)) module = require(path.resolve(modulePath)) diff --git a/package.json b/package.json index e47ebf2..a25495e 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "jsdoc-to-markdown": "^1.3.6", "koa-cache-control": "^1.0.0", "koa-livereload": "~0.2.0", - "node-fetch": "^1.5.3", "req-then": "~0.2.4", "tape": "^4.6.0" } From ac8994032c44a0dccd6628f59cb430b09f6b225a Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 5 Jul 2016 21:40:20 +0100 Subject: [PATCH 030/136] stack can now load from stored config --- lib/local-web-server.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 802d0b0..d9143e1 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -21,9 +21,12 @@ class LocalWebServer { const commandLineArgs = require('command-line-args') const commandLineUsage = require('command-line-usage') const cli = require('../lib/cli-data') + const loadConfig = require('config-master') + + const stored = loadConfig('local-web-server') /* manually scan for any --stack passed, as we may need to display stack options */ - const stackPaths = initOptions.stack || [] + const stackPaths = initOptions.stack || stored.stack || [] const stackIndex = process.argv.indexOf('--stack') if (stackIndex > -1) { for (var i = stackIndex + 1; i < process.argv.length; i++) { @@ -89,8 +92,6 @@ class LocalWebServer { } /* combine in stored config */ - const loadConfig = require('config-master') - const stored = loadConfig('local-web-server') options = Object.assign({ port: 8000 }, initOptions, stored, options.server, options.middleware, options.misc) this.options = options From 4baddce84cd6b30115cc182dc3d41772d93173d6 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 7 Jul 2016 22:56:27 +0100 Subject: [PATCH 031/136] can now construct with both stack paths or modules --- lib/local-web-server.js | 14 +++++++++++--- package.json | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index d9143e1..bbc60da 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -4,6 +4,7 @@ const ansi = require('ansi-escape-sequences') const path = require('path') const CommandLineTool = require('command-line-tool') const flatten = require('reduce-flatten') +const arrayify = require('array-back') /** * @module local-web-server @@ -26,7 +27,7 @@ class LocalWebServer { const stored = loadConfig('local-web-server') /* manually scan for any --stack passed, as we may need to display stack options */ - const stackPaths = initOptions.stack || stored.stack || [] + const stackPaths = arrayify(initOptions.stack || stored.stack) || [] const stackIndex = process.argv.indexOf('--stack') if (stackIndex > -1) { for (var i = stackIndex + 1; i < process.argv.length; i++) { @@ -39,8 +40,10 @@ class LocalWebServer { } } - /* load the stack */ + /* if the user did not supply a stack, use the default */ if (!stackPaths.length) stackPaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) + + /* load the stack */ const stackModules = stackPaths .map(stackPath => loadStack(stackPath)) .map(Middleware => new Middleware()) @@ -216,6 +219,7 @@ function getIPList () { */ function loadStack (modulePath) { let module + if (isModule(modulePath)) return modulePath const tried = [] if (modulePath) { try { @@ -237,7 +241,7 @@ function loadStack (modulePath) { } } if (module) { - if (!(module.prototype.middleware || module.prototype.stack)) { + if (!isModule(module)) { const insp = require('util').inspect(module, { depth: 3, colors: true }) const msg = `Not valid Middleware at: ${insp}` tool.halt(new Error(msg)) @@ -249,4 +253,8 @@ function loadStack (modulePath) { return module } +function isModule (module) { + return module.prototype.middleware || module.prototype.stack +} + module.exports = LocalWebServer diff --git a/package.json b/package.json index a25495e..4669e6b 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ "engines": { "node": ">=4.0.0" }, + "files": [ + "bin", "lib", "ssl" + ], "scripts": { "test": "tape test/*/*.js", "docs": "jsdoc2md -t jsdoc2md/api.hbs -p list lib/*.js > doc/api.md; echo", From 7bb45ab0aef15c8dcf27969ad1d4982891ec24ba Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 11 Jul 2016 13:43:34 +0100 Subject: [PATCH 032/136] options fix.. remove redundant tests --- lib/local-web-server.js | 7 +- package.json | 2 +- test/compress/big-file.txt | 195 ---------------------------------------- test/compress/compress.js | 22 ----- test/forbid/forbid.js | 21 ----- test/forbid/one.html | 1 - test/forbid/two.php | 1 - test/log/log.js | 25 ------ test/mime/mime.js | 22 ----- test/mime/something.php | 1 - test/mock/mock.js | 150 ------------------------------- test/mock/one.html | 1 - test/proxy/file.txt | 1 - test/proxy/one.html | 1 - test/proxy/rewrite-proxy.js | 73 --------------- test/rewrite/one.html | 1 - test/rewrite/rewrite.js | 19 ---- test/serve-index/serve-index.js | 18 ---- test/spa/one.txt | 1 - test/spa/spa.js | 28 ------ test/spa/two.txt | 1 - test/static/file.txt | 1 - test/static/static.js | 18 ---- test/test.js | 22 ++--- 24 files changed, 18 insertions(+), 614 deletions(-) delete mode 100644 test/compress/big-file.txt delete mode 100644 test/compress/compress.js delete mode 100644 test/forbid/forbid.js delete mode 100644 test/forbid/one.html delete mode 100644 test/forbid/two.php delete mode 100644 test/log/log.js delete mode 100644 test/mime/mime.js delete mode 100644 test/mime/something.php delete mode 100644 test/mock/mock.js delete mode 100644 test/mock/one.html delete mode 100644 test/proxy/file.txt delete mode 100644 test/proxy/one.html delete mode 100644 test/proxy/rewrite-proxy.js delete mode 100644 test/rewrite/one.html delete mode 100644 test/rewrite/rewrite.js delete mode 100644 test/serve-index/serve-index.js delete mode 100644 test/spa/one.txt delete mode 100644 test/spa/spa.js delete mode 100644 test/spa/two.txt delete mode 100644 test/static/file.txt delete mode 100644 test/static/static.js diff --git a/lib/local-web-server.js b/lib/local-web-server.js index bbc60da..8d4ee06 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -95,9 +95,11 @@ class LocalWebServer { } /* combine in stored config */ - options = Object.assign({ port: 8000 }, initOptions, stored, options.server, options.middleware, options.misc) + options = Object.assign({ port: 8000 }, initOptions, stored || {}, options.server, options.middleware, options.misc) this.options = options + // console.log(initOptions, stored, options) + if (options.verbose) { // debug.setLevel(1) } @@ -176,6 +178,7 @@ class LocalWebServer { listen () { const options = this.options const server = this._server = this.getServer() + // console.log(options) return new Promise((resolve, reject) => { server.listen(options.port, () => { onServerUp(options.port, options['static.root'], server.isHttps) @@ -254,7 +257,7 @@ function loadStack (modulePath) { } function isModule (module) { - return module.prototype.middleware || module.prototype.stack + return module.prototype && (module.prototype.middleware || module.prototype.stack) } module.exports = LocalWebServer diff --git a/package.json b/package.json index 4669e6b..75e51a1 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "bin", "lib", "ssl" ], "scripts": { - "test": "tape test/*/*.js", + "test": "tape test/*.js", "docs": "jsdoc2md -t jsdoc2md/api.hbs -p list lib/*.js > doc/api.md; echo", "cover": "istanbul cover ./node_modules/.bin/tape test/*.js && cat coverage/lcov.info | coveralls && rm -rf coverage; echo" }, diff --git a/test/compress/big-file.txt b/test/compress/big-file.txt deleted file mode 100644 index e837549..0000000 --- a/test/compress/big-file.txt +++ /dev/null @@ -1,195 +0,0 @@ -[![view on npm](http://img.shields.io/npm/v/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) -[![npm module downloads per month](http://img.shields.io/npm/dm/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) -[![Dependency Status](https://david-dm.org/75lb/local-web-server.svg)](https://david-dm.org/75lb/local-web-server) -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) - -# local-web-server -Fires up a simple, CORS-enabled, static web server on a given port. Use for local web development or file sharing (directory browsing enabled). - -![local-web-server](http://75lb.github.io/local-web-server/ws.gif) - -## Install -Ensure [node.js](http://nodejs.org) is installed first. Linux/Mac users may need to run the following commands with `sudo`. - -### Globally -```sh -$ npm install -g local-web-server -``` - -### Bundled with your project -```sh -$ npm install local-web-server --save-dev -``` - -Then add an `start` script to your `package.json` (the standard npm approach): -```json -{ - "name": "my-web-app", - "version": "1.0.0", - "scripts": { - "start": "ws" - } -} -``` -This simplifies a rather specific-looking instruction set like: - -```sh -$ npm install -$ npm install -g local-web-server -$ ws -``` - -to the following, server implementation and launch details abstracted away: -```sh -$ npm install -$ npm start -``` - -## Usage -``` -Usage -$ ws -$ ws --config -$ ws --help - -Server --p, --port Web server port --f, --log.format If a format is supplied an access log is written to stdout. If not, a statistics view is displayed. Use a - preset ('none', 'dev','combined', 'short', 'tiny' or 'logstalgia') or supply a custom format (e.g. ':method -> - :url'). --d, --directory Root directory, defaults to the current directory --c, --compress Enable gzip compression, reduces bandwidth. --r, --refresh-rate Statistics view refresh rate in ms. Defaults to 500. - -Misc --h, --help Print these usage instructions ---config Print the stored config -``` - -From the folder you wish to serve, run: -```sh -$ ws -serving at http://localhost:8000 -``` - -If you wish to serve a different directory, run: -```sh -$ ws -d ~/mysite/ -serving /Users/Lloyd/mysite at http://localhost:8000 -``` - -If you wish to override the default port (8000), use `--port` or `-p`: -```sh -$ ws --port 9000 -serving at http://localhost:9000 -``` - -To add compression, reducing bandwidth, increasing page load time (by 10-15% on my Macbook Air) -```sh -$ ws --compress -``` - -### Logging -Passing a value to `--log.format` will write an access log to `stdout`. - -Either use a built-in [morgan](https://github.com/expressjs/morgan) logger preset: -```sh -$ ws --log.format short -``` - -Or a custom [morgan](https://github.com/expressjs/morgan) log format: -```sh -$ ws -f ':method -> :url' -``` - -Or silence: -```sh -$ ws -f none -``` - -## Storing default options -To store per-project options, saving you the hassle of inputting them everytime, store them in the `local-web-server` property of your project's `package.json`: -```json -{ - "name": "my-project", - "version": "0.11.8", - "local-web-server":{ - "port": 8100 - } -} -``` - -Or in a `.local-web-server.json` file stored in the directory you want to serve (typically the root folder of your site): -```json -{ - "port": 8100, - "log.format": "tiny" -} -``` - -Or store global defaults in a `.local-web-server.json` file in your home directory. -```json -{ - "port": 3000, - "refresh-rate": 1000 -} -``` - -All stored defaults are overriden by options supplied at the command line. - -To view your stored defaults, run: - -```sh -$ ws --config -``` - -## mime-types -You can set additional mime-type/extension mappings, or override the defaults by setting a `mime` value in your local config. This value is passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine). Example: - -```json -{ - "mime": { - "text/plain": [ "php", "pl" ] - } -} -``` - -## Use with Logstalgia -local-web-server is compatible with [logstalgia](http://code.google.com/p/logstalgia/). - -### Install Logstalgia -On MacOSX, install with [homebrew](http://brew.sh): -```sh -$ brew install logstalgia -``` - -Alternatively, [download a release for your system from github](https://github.com/acaudwell/Logstalgia/releases/latest). - -Then pipe the `logstalgia` output format directly into logstalgia for real-time visualisation: -```sh -$ ws -f logstalgia | logstalgia - -``` - -![local-web-server with logstalgia](http://75lb.github.io/local-web-server/logstagia.gif) - -## Use with glTail -To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format: -```sh -$ ws -f default > web.log -``` - -Then specify this file in your glTail config: - -```yaml -servers: - dev: - host: localhost - source: local - files: /Users/Lloyd/Documents/MySite/web.log - parser: apache - color: 0.2, 0.2, 1.0, 1.0 -``` - -* * * - -© 2015 Lloyd Brookes <75pound@gmail.com> diff --git a/test/compress/compress.js b/test/compress/compress.js deleted file mode 100644 index 84f057b..0000000 --- a/test/compress/compress.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('compress', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addCompression(true) - ws.addStatic(__dirname) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/big-file.txt', { headers: { 'Accept-Encoding': 'gzip' } }) - .then(response => { - t.strictEqual(response.res.statusCode, 200) - t.strictEqual(response.res.headers['content-encoding'], 'gzip') - }) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/forbid/forbid.js b/test/forbid/forbid.js deleted file mode 100644 index c2d4910..0000000 --- a/test/forbid/forbid.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('forbid', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addBlacklist([ '*.php', '*.html' ]) - ws.addStatic(__dirname) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/two.php') - .then(c.checkResponse(t, 403)) - .then(() => request('http://localhost:8100/one.html')) - .then(c.checkResponse(t, 403)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/forbid/one.html b/test/forbid/one.html deleted file mode 100644 index 5626abf..0000000 --- a/test/forbid/one.html +++ /dev/null @@ -1 +0,0 @@ -one diff --git a/test/forbid/two.php b/test/forbid/two.php deleted file mode 100644 index abb2fca..0000000 --- a/test/forbid/two.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/log/log.js b/test/log/log.js deleted file mode 100644 index a90e2f6..0000000 --- a/test/log/log.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('logging', function (t) { - t.plan(2) - const ws = new LocalWebServer() - - const stream = require('stream').PassThrough() - stream.on('readable', () => { - let chunk = stream.read() - if (chunk) t.ok(/GET/.test(chunk.toString())) - }) - - ws.addLogging('common', { stream: stream }) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/') - .then(c.checkResponse(t, 404)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/mime/mime.js b/test/mime/mime.js deleted file mode 100644 index bb8cc44..0000000 --- a/test/mime/mime.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('mime override', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addMimeOverride({ 'text/plain': [ 'php' ] }) - ws.addStatic(__dirname) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/something.php') - .then(response => { - t.strictEqual(response.res.statusCode, 200) - t.ok(/text\/plain/.test(response.res.headers['content-type'])) - }) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/mime/something.php b/test/mime/something.php deleted file mode 100644 index abb2fca..0000000 --- a/test/mime/something.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/mock/mock.js b/test/mock/mock.js deleted file mode 100644 index 28dccac..0000000 --- a/test/mock/mock.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('mock: simple response', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addMockResponses([ - { route: '/test', response: { body: 'test' } } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(c.checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) - -test('mock: method request filter', function (t) { - t.plan(3) - const ws = new LocalWebServer() - ws.addMockResponses([ - { - route: '/test', - request: { method: 'POST' }, - response: { body: 'test' } - } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(c.checkResponse(t, 404)) - .then(() => request('http://localhost:8100/test', { data: 'something' })) - .then(c.checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) - -test('mock: accepts request filter', function (t) { - t.plan(3) - const ws = new LocalWebServer() - ws.addMockResponses([ - { - route: '/test', - request: { accepts: 'text' }, - response: { body: 'test' } - } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test', { headers: { Accept: '*/json' } }) - .then(c.checkResponse(t, 404)) - .then(() => request('http://localhost:8100/test', { headers: { Accept: 'text/plain' } })) - .then(c.checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: responses array', function (t) { - t.plan(4) - const ws = new LocalWebServer() - ws.addMockResponses([ - { - route: '/test', - responses: [ - { request: { method: 'GET' }, response: { body: 'get' } }, - { request: { method: 'POST' }, response: { body: 'post' } } - ] - } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(c.checkResponse(t, 200, /get/)) - .then(() => request('http://localhost:8100/test', { method: 'POST' })) - .then(c.checkResponse(t, 200, /post/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: response function', function (t) { - t.plan(4) - const ws = new LocalWebServer() - ws.addMockResponses([ - { - route: '/test', - responses: [ - { request: { method: 'GET' }, response: ctx => ctx.body = 'get' }, - { request: { method: 'POST' }, response: ctx => ctx.body = 'post' } - ] - } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(c.checkResponse(t, 200, /get/)) - .then(() => request('http://localhost:8100/test', { method: 'POST' })) - .then(c.checkResponse(t, 200, /post/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: response function args', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addMockResponses([ - { - route: '/test/:one', - responses: [ - { request: { method: 'GET' }, response: (ctx, one) => ctx.body = one } - ] - } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test/yeah') - .then(c.checkResponse(t, 200, /yeah/)) - .then(server.close.bind(server)) - }) -}) - -test('mock: async response function', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addMockResponses([ - { - route: '/test', - responses: { - response: function (ctx) { - return new Promise((resolve, reject) => { - setTimeout(() => { - ctx.body = 'test' - resolve() - }, 10) - }) - } - } - } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test') - .then(c.checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - }) -}) diff --git a/test/mock/one.html b/test/mock/one.html deleted file mode 100644 index 5626abf..0000000 --- a/test/mock/one.html +++ /dev/null @@ -1 +0,0 @@ -one diff --git a/test/proxy/file.txt b/test/proxy/file.txt deleted file mode 100644 index 5626abf..0000000 --- a/test/proxy/file.txt +++ /dev/null @@ -1 +0,0 @@ -one diff --git a/test/proxy/one.html b/test/proxy/one.html deleted file mode 100644 index 5626abf..0000000 --- a/test/proxy/one.html +++ /dev/null @@ -1 +0,0 @@ -one diff --git a/test/proxy/rewrite-proxy.js b/test/proxy/rewrite-proxy.js deleted file mode 100644 index 3fea168..0000000 --- a/test/proxy/rewrite-proxy.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const http = require('http') -const c = require('../common') - -test('rewrite: proxy', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addRewrite([ - { from: '/test/*', to: 'http://registry.npmjs.org/$1' } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test/') - .then(c.checkResponse(t, 200, /db_name/)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) - -test('rewrite: proxy, POST', function (t) { - t.plan(1) - const ws = new LocalWebServer() - ws.addRewrite([ - { from: '/test/*', to: 'http://registry.npmjs.org/' } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/test/', { data: {} }) - .then(c.checkResponse(t, 405)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) - -test('rewrite: proxy, two url tokens', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addRewrite([ - { from: '/:package/:version', to: 'http://registry.npmjs.org/:package/:version' } - ]) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/command-line-args/1.0.0') - .then(c.checkResponse(t, 200, /command-line-args/)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) - -test('rewrite: proxy with port', function (t) { - t.plan(2) - const ws1 = new LocalWebServer() - ws1.addStatic(__dirname) - - const ws2 = new LocalWebServer() - ws2.addRewrite([ - { from: '/test/*', to: 'http://localhost:9000/$1' } - ]) - const server1 = ws1.getServer() - const server2 = ws2.getServer() - server1.listen(9000, () => { - server2.listen(8100, () => { - request('http://localhost:8100/test/file.txt') - .then(c.checkResponse(t, 200, /one/)) - .then(server1.close.bind(server1)) - .then(server2.close.bind(server2)) - .catch(c.fail(t)) - }) - }) -}) diff --git a/test/rewrite/one.html b/test/rewrite/one.html deleted file mode 100644 index 5626abf..0000000 --- a/test/rewrite/one.html +++ /dev/null @@ -1 +0,0 @@ -one diff --git a/test/rewrite/rewrite.js b/test/rewrite/rewrite.js deleted file mode 100644 index cdce7e0..0000000 --- a/test/rewrite/rewrite.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('rewrite local', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addRewrite([ { from: '/two.html', to: '/one.html' } ]) - ws.addStatic(__dirname) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/two.html') - .then(c.checkResponse(t, 200, /one/)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/serve-index/serve-index.js b/test/serve-index/serve-index.js deleted file mode 100644 index 3ca09d6..0000000 --- a/test/serve-index/serve-index.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('static', function (t) { - t.plan(3) - const ws = new LocalWebServer() - ws.addIndex(__dirname, { icons: true }) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/') - .then(c.checkResponse(t, 200, [ /listing directory/, /class="icon/ ])) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/spa/one.txt b/test/spa/one.txt deleted file mode 100644 index 5626abf..0000000 --- a/test/spa/one.txt +++ /dev/null @@ -1 +0,0 @@ -one diff --git a/test/spa/spa.js b/test/spa/spa.js deleted file mode 100644 index 38dbc50..0000000 --- a/test/spa/spa.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('spa', function (t) { - t.plan(6) - const ws = new LocalWebServer() - ws.addSpa('one.txt') - ws.addStatic(__dirname) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/asdf', { headers: { accept: 'text/html' } }) - .then(c.checkResponse(t, 200, /one/)) - /* html requests for missing files with extensions do not redirect to spa */ - .then(() => request('http://localhost:8100/asdf.txt', { headers: { accept: 'text/html' } })) - .then(c.checkResponse(t, 404)) - /* existing static file */ - .then(() => request('http://localhost:8100/two.txt')) - .then(c.checkResponse(t, 200, /two/)) - /* not a text/html request - does not redirect to spa */ - .then(() => request('http://localhost:8100/asdf', { headers: { accept: 'application/json' } })) - .then(c.checkResponse(t, 404)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/spa/two.txt b/test/spa/two.txt deleted file mode 100644 index f719efd..0000000 --- a/test/spa/two.txt +++ /dev/null @@ -1 +0,0 @@ -two diff --git a/test/static/file.txt b/test/static/file.txt deleted file mode 100644 index 9daeafb..0000000 --- a/test/static/file.txt +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/test/static/static.js b/test/static/static.js deleted file mode 100644 index 92cf1f8..0000000 --- a/test/static/static.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' -const test = require('tape') -const request = require('req-then') -const LocalWebServer = require('../../') -const c = require('../common') - -test('static', function (t) { - t.plan(2) - const ws = new LocalWebServer() - ws.addStatic(__dirname, { index: 'file.txt' }) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/') - .then(c.checkResponse(t, 200, /test/)) - .then(server.close.bind(server)) - .catch(c.fail(t)) - }) -}) diff --git a/test/test.js b/test/test.js index a8a7a7c..23c626b 100644 --- a/test/test.js +++ b/test/test.js @@ -8,15 +8,17 @@ const path = require('path') test('stack', function (t) { t.plan(2) const ws = new LocalWebServer({ - stack: [ path.resolve(__dirname, 'test-middleware.js') ] - }) - const server = ws.getServer() - server.listen(8100, () => { - request('http://localhost:8100/') - .then(c.checkResponse(t, 200, /1234512345/)) - .then(server.close.bind(server)) - .catch(c.fail(t)) + stack: [ path.resolve(__dirname, 'test-middleware.js') ], + port: 8100 }) + ws.listen() + .then(() => { + return request('http://localhost:8100/') + .then(c.checkResponse(t, 200, /1234512345/)) + .then(ws.close.bind(ws)) + .catch(c.fail(t)) + }) + .catch(c.fail(t)) }) test('https', function (t) { @@ -28,9 +30,9 @@ test('https', function (t) { }) ws.listen() .then(() => { - request('https://localhost:8100/') + return request('https://localhost:8100/') .then(c.checkResponse(t, 200, /1234512345/)) .then(ws.close.bind(ws)) - .catch(c.fail(t)) }) + .catch(c.fail(t)) }) From c71b2267b7c845339fec93e6f3814f30a90614da Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 11 Jul 2016 22:19:04 +0100 Subject: [PATCH 033/136] ignoreCli option.. fixed tests.. removed .listen() and close().. refactor --- bin/cli.js | 35 +++++++++++++++++++-- lib/local-web-server.js | 82 +++++++++++++++++-------------------------------- test/test.js | 32 +++++++++---------- 3 files changed, 75 insertions(+), 74 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 54493ed..1a4a6ff 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,7 +1,36 @@ #!/usr/bin/env node 'use strict' -const LocalWebServer = require('../') +const path = require('path') + +function onServerUp (port, directory, isHttps) { + const ansi = require('ansi-escape-sequences') + + const ipList = getIPList() + .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${port}}`) + .join(', ') + + console.error(ansi.format( + path.resolve(directory || '') === process.cwd() + ? `serving at ${ipList}` + : `serving [underline]{${directory}} at ${ipList}` + )) +} +function getIPList () { + const flatten = require('reduce-flatten') + const os = require('os') + + let ipList = Object.keys(os.networkInterfaces()) + .map(key => os.networkInterfaces()[key]) + .reduce(flatten, []) + .filter(iface => iface.family === 'IPv4') + ipList.unshift({ address: os.hostname() }) + return ipList +} + +const LocalWebServer = require('../') const ws = new LocalWebServer() -ws.listen() - .catch(err => console.error(err.stack)) +const server = ws.getServer() +server.on('listening', function () { + onServerUp(ws.options.port, ws.options['static.root'], server.isHttps) +}) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 8d4ee06..c45e55a 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,6 +1,5 @@ #!/usr/bin/env node 'use strict' -const ansi = require('ansi-escape-sequences') const path = require('path') const CommandLineTool = require('command-line-tool') const flatten = require('reduce-flatten') @@ -83,23 +82,35 @@ class LocalWebServer { let options = {} const allOptionDefinitions = cli.optionDefinitions.concat(middlewareOptionDefinitions) - try { - options = commandLineArgs(allOptionDefinitions) - } catch (err) { - tool.printError(err) - tool.printError(allOptionDefinitions.map(def => { - return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` - }).join('\n')) - console.error(usage) - tool.halt() + if (!initOptions.ignoreCli) { + try { + options = commandLineArgs(allOptionDefinitions) + } catch (err) { + tool.printError(err) + tool.printError(allOptionDefinitions.map(def => { + return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` + }).join('\n')) + console.error(usage) + tool.halt() + } } /* combine in stored config */ - options = Object.assign({ port: 8000 }, initOptions, stored || {}, options.server, options.middleware, options.misc) + options = Object.assign( + { port: 8000 }, + initOptions, + stored, + options.server, + options.middleware, + options.misc + ) + + /** + * Config + * @type {object} + */ this.options = options - // console.log(initOptions, stored, options) - if (options.verbose) { // debug.setLevel(1) } @@ -146,9 +157,10 @@ class LocalWebServer { return app } - getServer () { + getServer (onListening) { const app = this.getApplication() const options = this.options + let key = options.key let cert = options.cert @@ -172,50 +184,12 @@ class LocalWebServer { const http = require('http') server = http.createServer(app.callback()) } - return server - } - listen () { - const options = this.options - const server = this._server = this.getServer() - // console.log(options) - return new Promise((resolve, reject) => { - server.listen(options.port, () => { - onServerUp(options.port, options['static.root'], server.isHttps) - resolve(server) - }) - }) - } - - close () { - this._server.close() + server.listen(options.port, onListening) + return server } } -function onServerUp (port, directory, isHttps) { - const ipList = getIPList() - .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${port}}`) - .join(', ') - - console.error(ansi.format( - path.resolve(directory || '') === process.cwd() - ? `serving at ${ipList}` - : `serving [underline]{${directory}} at ${ipList}` - )) -} - -function getIPList () { - const flatten = require('reduce-flatten') - const os = require('os') - - let ipList = Object.keys(os.networkInterfaces()) - .map(key => os.networkInterfaces()[key]) - .reduce(flatten, []) - .filter(iface => iface.family === 'IPv4') - ipList.unshift({ address: os.hostname() }) - return ipList -} - /** * Loads a module by either path or name. * @returns {object} diff --git a/test/test.js b/test/test.js index 23c626b..cccbf62 100644 --- a/test/test.js +++ b/test/test.js @@ -9,16 +9,15 @@ test('stack', function (t) { t.plan(2) const ws = new LocalWebServer({ stack: [ path.resolve(__dirname, 'test-middleware.js') ], - port: 8100 + port: 8100, + ignoreCli: true + }) + const server = ws.getServer(() => { + return request('http://localhost:8100/') + .then(c.checkResponse(t, 200, /1234512345/)) + .then(server.close.bind(server)) + .catch(c.fail(t)) }) - ws.listen() - .then(() => { - return request('http://localhost:8100/') - .then(c.checkResponse(t, 200, /1234512345/)) - .then(ws.close.bind(ws)) - .catch(c.fail(t)) - }) - .catch(c.fail(t)) }) test('https', function (t) { @@ -26,13 +25,12 @@ test('https', function (t) { const ws = new LocalWebServer({ stack: [ path.resolve(__dirname, 'test-middleware.js') ], https: true, - port: 8100 + port: 8100, + ignoreCli: true + }) + const server = ws.getServer(() => { + return request('https://localhost:8100/') + .then(c.checkResponse(t, 200, /1234512345/)) + .then(server.close.bind(server)) }) - ws.listen() - .then(() => { - return request('https://localhost:8100/') - .then(c.checkResponse(t, 200, /1234512345/)) - .then(ws.close.bind(ws)) - }) - .catch(c.fail(t)) }) From 2033a13ce4a84e0091cc37dd7c2881f5c82a9cd0 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 12 Jul 2016 19:29:26 +0100 Subject: [PATCH 034/136] deps --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 75e51a1..cebd324 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "node": ">=4.0.0" }, "files": [ - "bin", "lib", "ssl" + "bin", + "lib", + "ssl" ], "scripts": { "test": "tape test/*.js", @@ -37,11 +39,12 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", - "command-line-tool": "75lb/command-line-tool", + "command-line-tool": "~0.4.0", "config-master": "^2.0.3", "koa": "^2.0.0", "local-web-server-default-stack": "github:local-web-server/default-stack", "reduce-flatten": "^1.0.1", + "table-layout": "~0.2.2", "typical": "^2.4.2", "walk-back": "^2.0.1" }, From 7a958a68ec5c33cb81e22df0654f250524a14ee5 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 12 Jul 2016 19:35:55 +0100 Subject: [PATCH 035/136] deps --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index cebd324..57a413c 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", + "command-line-args": "^3.0.0", "command-line-tool": "~0.4.0", + "command-line-usage": "^3.0.3", "config-master": "^2.0.3", "koa": "^2.0.0", "local-web-server-default-stack": "github:local-web-server/default-stack", From 7b489a456c98e349ed5ee46b4d6692e21b4be1d6 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 12 Jul 2016 19:40:04 +0100 Subject: [PATCH 036/136] deps --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 57a413c..be2976a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "command-line-usage": "^3.0.3", "config-master": "^2.0.3", "koa": "^2.0.0", + "koa-compose": "^3.1.0", + "koa-convert": "^1.2.0", "local-web-server-default-stack": "github:local-web-server/default-stack", "reduce-flatten": "^1.0.1", "table-layout": "~0.2.2", From ad4efaa6d7b3e1880bf2c33ab44d7468d73e9e71 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 14 Jul 2016 20:34:22 +0100 Subject: [PATCH 037/136] refactor.. --verbose and --debug functional --- bin/cli.js | 31 -------------- lib/cli-data.js | 6 ++- lib/local-web-server.js | 108 ++++++++++++++++++++++++++++++++++-------------- 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 1a4a6ff..f188551 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,36 +1,5 @@ #!/usr/bin/env node 'use strict' -const path = require('path') - -function onServerUp (port, directory, isHttps) { - const ansi = require('ansi-escape-sequences') - - const ipList = getIPList() - .map(iface => `[underline]{${isHttps ? 'https' : 'http'}://${iface.address}:${port}}`) - .join(', ') - - console.error(ansi.format( - path.resolve(directory || '') === process.cwd() - ? `serving at ${ipList}` - : `serving [underline]{${directory}} at ${ipList}` - )) -} - -function getIPList () { - const flatten = require('reduce-flatten') - const os = require('os') - - let ipList = Object.keys(os.networkInterfaces()) - .map(key => os.networkInterfaces()[key]) - .reduce(flatten, []) - .filter(iface => iface.family === 'IPv4') - ipList.unshift({ address: os.hostname() }) - return ipList -} - const LocalWebServer = require('../') const ws = new LocalWebServer() const server = ws.getServer() -server.on('listening', function () { - onServerUp(ws.options.port, ws.options['static.root'], server.isHttps) -}) diff --git a/lib/cli-data.js b/lib/cli-data.js index 716326b..c7a3e96 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -29,7 +29,11 @@ exports.optionDefinitions = [ }, { name: 'verbose', type: Boolean, alias: 'v', - description: 'Verbose output, useful for debugging.', group: 'misc' + description: 'Verbose output.', group: 'misc' + }, + { + name: 'debug', type: Boolean, + description: 'Very verbose output, intended for debugging.', group: 'misc' }, { name: 'version', type: Boolean, diff --git a/lib/local-web-server.js b/lib/local-web-server.js index c45e55a..25bd0bb 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -4,6 +4,7 @@ const path = require('path') const CommandLineTool = require('command-line-tool') const flatten = require('reduce-flatten') const arrayify = require('array-back') +const ansi = require('ansi-escape-sequences') /** * @module local-web-server @@ -42,30 +43,8 @@ class LocalWebServer { /* if the user did not supply a stack, use the default */ if (!stackPaths.length) stackPaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) - /* load the stack */ - const stackModules = stackPaths - .map(stackPath => loadStack(stackPath)) - .map(Middleware => new Middleware()) - .map(module => { - if (module.stack) { - const featureStack = module.stack() - module.optionDefinitions = function () { - return featureStack - .map(Feature => new Feature()) - .map(feature => feature.optionDefinitions && feature.optionDefinitions()) - .filter(definitions => definitions) - .reduce(flatten, []) - } - module.middleware = function (options) { - return featureStack - .map(Feature => new Feature()) - .map(feature => feature.middleware(options)) - .reduce(flatten, []) - .filter(mw => mw) - } - } - return module - }) + /* build the stack */ + const stackModules = buildStack(stackPaths, this.onVerbose.bind(this), this.onDebug.bind(this)) /* gather stack option definitions and parse the command line */ const middlewareOptionDefinitions = stackModules @@ -112,7 +91,14 @@ class LocalWebServer { this.options = options if (options.verbose) { - // debug.setLevel(1) + stackModules + .filter(mw => mw.on) + .forEach(mw => mw.on('verbose', onVerbose)) + } + if (options.debug) { + stackModules + .filter(mw => mw.on) + .forEach(mw => mw.on('debug', onDebug)) } /* --config */ @@ -147,12 +133,7 @@ class LocalWebServer { app.use(compose(middlewareStack)) app.on('error', err => { - const defaultLogInUse = this.stack.some(mw => mw.constructor.name === 'Log') - if (defaultLogInUse) { - if (this.options['log.format']) console.error(ansi.format(err.stack, 'red')) - } else { - console.error(ansi.format(err.stack, 'red')) - } + console.error(ansi.format(err.stack, 'red')) }) return app } @@ -185,9 +166,27 @@ class LocalWebServer { server = http.createServer(app.callback()) } - server.listen(options.port, onListening) + const tableLayout = require('table-layout') + + server.listen(options.port, function () { + const ipList = getIPList() + .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) + .join(', ') + console.error(ansi.format('Serving at', 'bold'), ansi.format(ipList)) + }) return server } + + onVerbose (title, msg) { + if (this.options.verbose) { + console.error(ansi.format(title, 'bold'), msg) + } + } + onDebug (title, msg) { + if (this.options.debug) { + console.error(ansi.format(title, 'bold'), msg) + } + } } /** @@ -234,4 +233,49 @@ function isModule (module) { return module.prototype && (module.prototype.middleware || module.prototype.stack) } +function getIPList () { + const flatten = require('reduce-flatten') + const os = require('os') + + let ipList = Object.keys(os.networkInterfaces()) + .map(key => os.networkInterfaces()[key]) + .reduce(flatten, []) + .filter(iface => iface.family === 'IPv4') + ipList.unshift({ address: os.hostname() }) + return ipList +} + +function buildStack (stackPaths, onVerbose, onDebug) { + return stackPaths + .map(stackPath => loadStack(stackPath)) + .map(Middleware => new Middleware()) + .map(module => { + if (module.stack) { + const featureStack = module.stack() + .map(Feature => new Feature()) + .map(feature => { + if (feature.on) { + feature.on('verbose', onVerbose) + feature.on('debug', onDebug) + } + return feature + }) + + module.optionDefinitions = function () { + return featureStack + .map(feature => feature.optionDefinitions && feature.optionDefinitions()) + .filter(definitions => definitions) + .reduce(flatten, []) + } + module.middleware = function (options) { + return featureStack + .map(feature => feature.middleware(options)) + .reduce(flatten, []) + .filter(mw => mw) + } + } + return module + }) +} + module.exports = LocalWebServer From 60e3608d625da197a1ac6d8d69c84eeaa19df06c Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 14 Jul 2016 23:00:46 +0100 Subject: [PATCH 038/136] fix tests --- bin/cli.js | 2 +- lib/local-web-server.js | 34 +++++++++++++++++----------------- package.json | 2 +- test/test.js | 18 ++++++++++++++---- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index f188551..35c5a3c 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -2,4 +2,4 @@ 'use strict' const LocalWebServer = require('../') const ws = new LocalWebServer() -const server = ws.getServer() +ws.getServer() diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 25bd0bb..a25358c 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -61,7 +61,7 @@ class LocalWebServer { let options = {} const allOptionDefinitions = cli.optionDefinitions.concat(middlewareOptionDefinitions) - if (!initOptions.ignoreCli) { + if (!initOptions.testMode) { try { options = commandLineArgs(allOptionDefinitions) } catch (err) { @@ -90,16 +90,12 @@ class LocalWebServer { */ this.options = options - if (options.verbose) { - stackModules - .filter(mw => mw.on) - .forEach(mw => mw.on('verbose', onVerbose)) - } - if (options.debug) { - stackModules - .filter(mw => mw.on) - .forEach(mw => mw.on('debug', onDebug)) - } + stackModules + .filter(mw => mw.on) + .forEach(mw => { + mw.on('verbose', this.onVerbose.bind(this)) + mw.on('debug', this.onDebug.bind(this)) + }) /* --config */ if (options.config) { @@ -168,12 +164,16 @@ class LocalWebServer { const tableLayout = require('table-layout') - server.listen(options.port, function () { - const ipList = getIPList() - .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) - .join(', ') - console.error(ansi.format('Serving at', 'bold'), ansi.format(ipList)) - }) + server + .listen(options.port) + .on('listening', function () { + if (options.testMode) return + const ipList = getIPList() + .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) + .join(', ') + console.error(ansi.format('Serving at', 'bold'), ansi.format(ipList)) + }) + .on('listening', onListening) return server } diff --git a/package.json b/package.json index be2976a..464552a 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "jsdoc-to-markdown": "^1.3.6", "koa-cache-control": "^1.0.0", "koa-livereload": "~0.2.0", - "req-then": "~0.2.4", + "req-then": "~0.3.3", "tape": "^4.6.0" } } diff --git a/test/test.js b/test/test.js index cccbf62..68a6d6d 100644 --- a/test/test.js +++ b/test/test.js @@ -10,13 +10,16 @@ test('stack', function (t) { const ws = new LocalWebServer({ stack: [ path.resolve(__dirname, 'test-middleware.js') ], port: 8100, - ignoreCli: true + testMode: true }) const server = ws.getServer(() => { return request('http://localhost:8100/') .then(c.checkResponse(t, 200, /1234512345/)) .then(server.close.bind(server)) - .catch(c.fail(t)) + .catch(err => { + t.fail(err.message) + server.close() + }) }) }) @@ -26,11 +29,18 @@ test('https', function (t) { stack: [ path.resolve(__dirname, 'test-middleware.js') ], https: true, port: 8100, - ignoreCli: true + testMode: true }) + const url = require('url') + const reqOptions = url.parse('https://localhost:8100/') + reqOptions.rejectUnauthorized = false const server = ws.getServer(() => { - return request('https://localhost:8100/') + return request(reqOptions) .then(c.checkResponse(t, 200, /1234512345/)) .then(server.close.bind(server)) + .catch(err => { + t.fail(err.message) + server.close() + }) }) }) From 906483b64910775f18a019e7638b9b3c72e360fa Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 14 Jul 2016 23:12:02 +0100 Subject: [PATCH 039/136] fix listening handler --- lib/local-web-server.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index a25358c..be19d98 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -164,16 +164,17 @@ class LocalWebServer { const tableLayout = require('table-layout') - server - .listen(options.port) - .on('listening', function () { - if (options.testMode) return + server.listen(options.port) + if (onListening) server.on('listening', onListening) + if (!options.testMode) { + server.on('listening', function () { const ipList = getIPList() .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) .join(', ') console.error(ansi.format('Serving at', 'bold'), ansi.format(ipList)) }) - .on('listening', onListening) + } + return server } From 4ce3374d9c3dc239886b3f2000e7b68cd02bc659 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 16 Jul 2016 09:59:11 +0100 Subject: [PATCH 040/136] readme, remove old examples --- README.md | 64 ++++++++++----------------- example/stack/cache-control.js | 27 ----------- example/stack/live-reload-optional/index.html | 10 ----- example/stack/live-reload-optional/stack.js | 24 ---------- example/stack/live-reload/index.html | 10 ----- example/stack/live-reload/stack.js | 14 ------ 6 files changed, 24 insertions(+), 125 deletions(-) delete mode 100644 example/stack/cache-control.js delete mode 100644 example/stack/live-reload-optional/index.html delete mode 100644 example/stack/live-reload-optional/stack.js delete mode 100644 example/stack/live-reload/index.html delete mode 100644 example/stack/live-reload/stack.js diff --git a/README.md b/README.md index f2b379f..5f1ceb3 100644 --- a/README.md +++ b/README.md @@ -8,28 +8,33 @@ ***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.*** # local-web-server -An application shell for building a simple, command-line web server for productive web development. It contains no middleware of its own but will load default-stack unless you specify otherwise. +At its core, local-web-server is an application shell for building a specialised command-line web server to support productive Web Platform engineers. It comes bundled with a middleware stack covering common requirements but any arbitrary stack can be specified from the command line or config. -It is trivial is bundle and deploy with your project. Also deploys to heroku well for demo projects. +Being an npm module, it is trivial is bundle and distribute/deploy with your web application. -It comes with some middleware built-in, which you need not use but will get you up and running for the following use cases: +**Typically used for building:** -* Static or Single Page Application front-end development where you have - * No backend, an existing remote API or need to mock-up an API. +* Simple static site +* Single Page Application + * Works well with React, Angular or vanilla JS. -Application Shell - * HTTP or HTTPS server - * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.) - * Add your middleware - * Use any combination of built-in and custom middleware - * specify options (for command line or config) - * Accepts Koa v1 or 2 middleware - * Bundle with your front-end project - * Configuration is via json file or command-line (latter taking precedence) - * Outputs a dynamic statistics view to the terminal +**Backend scenarios covered:** +* Existing API +* Mock API +* Websocket server + +**Server options** + +* HTTP or HTTPS server + * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.) +* Configurable middleware stack + * Use any combination of built-in and custom middleware + * specify options (for command line or config) + * Accepts Koa v1 or 2 middleware + +**Built-in Middleware stack** -Built-in Middleware (all optional) * Rewrite routes to local or remote resources * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md) @@ -40,6 +45,9 @@ Built-in Middleware (all optional) * Mocks are defined with config (static), or code (dynamic). * CORS-friendly, all origins allowed by default. +**Personalised stack** + + ## Synopsis local-web-server is a command-line tool. To serve the current directory, run `ws`. @@ -86,32 +94,8 @@ local-web-server is a command-line tool. To serve the current directory, run `ws Project home: https://github.com/75lb/local-web-server
    -## Examples - -For the examples below, we assume we're in a project directory looking like this: - -```sh -. -├── css -│   └── style.css -├── index.html -└── package.json -``` - -All paths/routes are specified using [express syntax](http://expressjs.com/guide/routing.html#route-paths). To run the example projects linked below, clone the project, move into the example directory specified, run `ws`. - -### Static site - -Fire up your static site on the default port: -```sh -$ ws -serving at http://localhost:8000 -``` - -[Example](https://github.com/75lb/local-web-server/tree/master/example/simple). ## Install -Ensure [node.js](http://nodejs.org) is installed first. Linux/Mac users may need to run the following commands with `sudo`. ```sh $ npm install -g local-web-server diff --git a/example/stack/cache-control.js b/example/stack/cache-control.js deleted file mode 100644 index 074a52e..0000000 --- a/example/stack/cache-control.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict' -const LocalWebServer = require('../../') -const cacheControl = require('koa-cache-control') -const DefaultStack = require('local-web-server-default-stack') - -class CacheControl extends DefaultStack { - addAll () { - return this.addLogging('dev') - .addCacheControl() - .addStatic() - .addIndex() - } - addCacheControl () { - this.add({ - optionDefinitions: { - name: 'maxage', type: Number, - description: 'The maxage to set on each response.' - }, - middleware: function (options) { - return cacheControl({ maxAge: options.maxage }) - } - }) - return this - } -} - -module.exports = CacheControl diff --git a/example/stack/live-reload-optional/index.html b/example/stack/live-reload-optional/index.html deleted file mode 100644 index f467990..0000000 --- a/example/stack/live-reload-optional/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - live-reload demo - - -

    Live reloaded potentially attached

    - - diff --git a/example/stack/live-reload-optional/stack.js b/example/stack/live-reload-optional/stack.js deleted file mode 100644 index aa4f4ce..0000000 --- a/example/stack/live-reload-optional/stack.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' -const LocalWebServer = require('../../../') -const liveReload = require('koa-livereload') -const DefaultStack = require('local-web-server-default-stack') - -class LiveReloadStack extends DefaultStack { - addAll () { - return this.addLogging('dev') - .add({ - optionDefinitions: { - name: 'live-reload', type: Boolean, - description: 'Add live reload.' - }, - middleware: function (options) { - if (options['live-reload']) { - return liveReload() - } - } - }) - .addStatic() - } -} - -module.exports = LiveReloadStack diff --git a/example/stack/live-reload/index.html b/example/stack/live-reload/index.html deleted file mode 100644 index 0cc6591..0000000 --- a/example/stack/live-reload/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - live-reload demo - - -

    Live reloaded attached

    - - diff --git a/example/stack/live-reload/stack.js b/example/stack/live-reload/stack.js deleted file mode 100644 index 217a428..0000000 --- a/example/stack/live-reload/stack.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' -const LocalWebServer = require('../../../') -const liveReload = require('koa-livereload') -const DefaultStack = require('local-web-server-default-stack') - -class LiveReloadStack extends DefaultStack { - addAll () { - return this.addLogging('dev') - .add({ middleware: liveReload }) - .addStatic() - } -} - -module.exports = LiveReloadStack From 87f4a57ef8ce598ed954149d14ab93a1a29ddba4 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 16 Jul 2016 16:05:07 +0100 Subject: [PATCH 041/136] refactor.. added CliView.. removed cli-tool.. --- example/features/vanilla.js | 65 ++++++++++++++ lib/cli-data.js | 2 +- lib/local-web-server.js | 204 ++++++++++++++++++++++++++------------------ package.json | 1 - 4 files changed, 187 insertions(+), 85 deletions(-) create mode 100644 example/features/vanilla.js diff --git a/example/features/vanilla.js b/example/features/vanilla.js new file mode 100644 index 0000000..1617454 --- /dev/null +++ b/example/features/vanilla.js @@ -0,0 +1,65 @@ +'use strict' + +class Yeah { + middleware () { + return function (req, res, next) { + res.end('Yeah?') + next() + } + } +} + +class Logger { + middleware () { + const express = require('express') + const app = express() + app.use((req, res, next) => { + console.log('incoming', req.url) + next() + }) + return app + } +} + +class Header { + middleware () { + return function (req, res, next) { + res.setHeader('x-pointless', 'yeah?') + next() + } + } +} + +class PieHeader { + middleware () { + const Koa = require('koa') + const app = new Koa() + app.use((ctx, next) => { + ctx.set('x-pie', 'steak and kidney') + next() + }) + return app.callback() + } +} + +const http = require('http') +const server = http.createServer() +server.listen(8100) +const yeah = new Yeah() +const logger = new Logger() +const header = new Header() +const pie = new PieHeader() +const stack = [ + logger.middleware(), + header.middleware(), + pie.middleware(), + yeah.middleware() +] +server.on('request', function (req, res) { + let index = 0 + function processNext () { + const mw = stack[index++] + if (mw) mw(req, res, processNext) + } + processNext() +}) diff --git a/lib/cli-data.js b/lib/cli-data.js index c7a3e96..bbe4605 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -5,7 +5,7 @@ exports.optionDefinitions = [ }, { name: 'stack', type: String, multiple: true, - description: 'Middleware stack.', group: 'server' + description: 'Feature stack.', group: 'server' }, { name: 'key', type: String, typeLabel: '[underline]{file}', group: 'server', diff --git a/lib/local-web-server.js b/lib/local-web-server.js index be19d98..7672894 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,7 +1,6 @@ #!/usr/bin/env node 'use strict' const path = require('path') -const CommandLineTool = require('command-line-tool') const flatten = require('reduce-flatten') const arrayify = require('array-back') const ansi = require('ansi-escape-sequences') @@ -10,44 +9,68 @@ const ansi = require('ansi-escape-sequences') * @module local-web-server */ -const tool = new CommandLineTool() + +class CliView { + constructor (options) { + this.options = options || {} + } + show (key, value) { + if (key && value) { + const ansi = require('ansi-escape-sequences') + const tableLayout = require('table-layout') + const output = tableLayout({ key: ansi.format(key, 'bold'), value: value}, { + padding: { left: '', right: ' ' }, + columns: [ + { name: 'key', width: 18 }, + { name: 'value', nowrap: true } + ] + }) + process.stderr.write(output) + } else { + console.error(key) + } + } + verbose (key, value) { + if (this.options.verbose) { + this.show(key, value) + } + } + error (msg) { + console.error(ansi.format(msg, 'red')) + } +} /** * @alias module:local-web-server * @extends module:middleware-stack */ class LocalWebServer { + + /** + * @param [options] {object} - Server options + * @param [options.port} {number} - Port + * @param [options.stack} {string[]|Features[]} - Port + */ constructor (initOptions) { initOptions = initOptions || {} const commandLineArgs = require('command-line-args') const commandLineUsage = require('command-line-usage') const cli = require('../lib/cli-data') - const loadConfig = require('config-master') - const stored = loadConfig('local-web-server') + this.view = new CliView() - /* manually scan for any --stack passed, as we may need to display stack options */ - const stackPaths = arrayify(initOptions.stack || stored.stack) || [] - const stackIndex = process.argv.indexOf('--stack') - if (stackIndex > -1) { - for (var i = stackIndex + 1; i < process.argv.length; i++) { - const stackPath = process.argv[i] - if (/^-/.test(stackPath)) { - break - } else { - stackPaths.push(stackPath) - } - } - } + /* get stored config */ + const loadConfig = require('config-master') + const stored = loadConfig('local-web-server') - /* if the user did not supply a stack, use the default */ - if (!stackPaths.length) stackPaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) + /* read the config and command-line for feature paths */ + const featurePaths = parseFeaturePaths(initOptions.stack || stored.stack) - /* build the stack */ - const stackModules = buildStack(stackPaths, this.onVerbose.bind(this), this.onDebug.bind(this)) + /* load features and build the middleware stack */ + const features = this._buildFeatureStack(featurePaths) - /* gather stack option definitions and parse the command line */ - const middlewareOptionDefinitions = stackModules + /* gather feature optionDefinitions and parse the command line */ + const featureOptionDefinitions = features .filter(mw => mw.optionDefinitions) .map(mw => mw.optionDefinitions()) .reduce(flatten, []) @@ -57,20 +80,23 @@ class LocalWebServer { return def }) - const usage = commandLineUsage(cli.usage(middlewareOptionDefinitions)) + const usage = commandLineUsage(cli.usage(featureOptionDefinitions)) let options = {} - const allOptionDefinitions = cli.optionDefinitions.concat(middlewareOptionDefinitions) + const allOptionDefinitions = cli.optionDefinitions.concat(featureOptionDefinitions) if (!initOptions.testMode) { try { options = commandLineArgs(allOptionDefinitions) } catch (err) { - tool.printError(err) - tool.printError(allOptionDefinitions.map(def => { - return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` - }).join('\n')) - console.error(usage) - tool.halt() + this.view.error(err.toString()) + if (err.name === 'DUPLICATE_NAME') { + this.view.error('\nOption Definitions:') + this.view.error(allOptionDefinitions.map(def => { + return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` + }).join('\n')) + } + this.view.show(usage) + process.exit(1) } } @@ -84,33 +110,38 @@ class LocalWebServer { options.misc ) + this.view.options.verbose = options.verbose + /** * Config * @type {object} */ this.options = options - stackModules + this.features = features + + features .filter(mw => mw.on) .forEach(mw => { - mw.on('verbose', this.onVerbose.bind(this)) - mw.on('debug', this.onDebug.bind(this)) + mw.on('verbose', this.view.verbose.bind(this.view)) + mw.on('debug', this.view.verbose.bind(this.view)) }) /* --config */ if (options.config) { - tool.stop(JSON.stringify(options, null, ' '), 0) + this.view.show(JSON.stringify(options, null, ' ')) + process.exit(0) /* --version */ } else if (options.version) { const pkg = require(path.resolve(__dirname, '..', 'package.json')) - tool.stop(pkg.version) + this.view.show(pkg.version) + process.exit(0) /* --help */ } else if (options.help) { - tool.stop(usage) - } else { - this.stack = stackModules + this.view.show(usage) + process.exit(0) } } @@ -120,7 +151,7 @@ class LocalWebServer { const compose = require('koa-compose') const convert = require('koa-convert') - const middlewareStack = this.stack + const middlewareStack = this.features .filter(mw => mw.middleware) .map(mw => mw.middleware(this.options)) .reduce(flatten, []) @@ -162,31 +193,51 @@ class LocalWebServer { server = http.createServer(app.callback()) } - const tableLayout = require('table-layout') - server.listen(options.port) if (onListening) server.on('listening', onListening) if (!options.testMode) { - server.on('listening', function () { + server.on('listening', () => { const ipList = getIPList() .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) .join(', ') - console.error(ansi.format('Serving at', 'bold'), ansi.format(ipList)) + this.view.show('Serving at', ansi.format(ipList)) }) } return server } - onVerbose (title, msg) { - if (this.options.verbose) { - console.error(ansi.format(title, 'bold'), msg) - } - } - onDebug (title, msg) { - if (this.options.debug) { - console.error(ansi.format(title, 'bold'), msg) - } + _buildFeatureStack (featurePaths) { + return featurePaths + .map(featurePath => loadStack(featurePath)) + .map(Feature => new Feature()) + .map(module => { + if (module.stack) { + const featureStack = module.stack() + .map(Feature => new Feature()) + .map(feature => { + if (feature.on) { + feature.on('verbose', this.view.verbose.bind(this.view)) + feature.on('debug', this.view.verbose.bind(this.view)) + } + return feature + }) + + module.optionDefinitions = function () { + return featureStack + .map(feature => feature.optionDefinitions && feature.optionDefinitions()) + .filter(definitions => definitions) + .reduce(flatten, []) + } + module.middleware = function (options) { + return featureStack + .map(feature => feature.middleware(options)) + .reduce(flatten, []) + .filter(mw => mw) + } + } + return module + }) } } @@ -246,37 +297,24 @@ function getIPList () { return ipList } -function buildStack (stackPaths, onVerbose, onDebug) { - return stackPaths - .map(stackPath => loadStack(stackPath)) - .map(Middleware => new Middleware()) - .map(module => { - if (module.stack) { - const featureStack = module.stack() - .map(Feature => new Feature()) - .map(feature => { - if (feature.on) { - feature.on('verbose', onVerbose) - feature.on('debug', onDebug) - } - return feature - }) - - module.optionDefinitions = function () { - return featureStack - .map(feature => feature.optionDefinitions && feature.optionDefinitions()) - .filter(definitions => definitions) - .reduce(flatten, []) - } - module.middleware = function (options) { - return featureStack - .map(feature => feature.middleware(options)) - .reduce(flatten, []) - .filter(mw => mw) - } +/* manually scan for any --stack passed, as we may need to display stack options */ +function parseFeaturePaths (configStack) { + const featurePaths = arrayify(configStack) + const featureIndex = process.argv.indexOf('--stack') + if (featureIndex > -1) { + for (var i = featureIndex + 1; i < process.argv.length; i++) { + const featurePath = process.argv[i] + if (/^-/.test(featurePath)) { + break + } else { + featurePaths.push(featurePath) } - return module - }) + } + } + + /* if the user did not supply a stack, use the default */ + if (!featurePaths.length) featurePaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) + return featurePaths } module.exports = LocalWebServer diff --git a/package.json b/package.json index 464552a..347e776 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", "command-line-args": "^3.0.0", - "command-line-tool": "~0.4.0", "command-line-usage": "^3.0.3", "config-master": "^2.0.3", "koa": "^2.0.0", From f11cbee8205ddc1ec9f5d678f7f23e1dbd829c88 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 17 Jul 2016 12:25:50 +0100 Subject: [PATCH 042/136] docs.. view work.. refactor --- README.md | 2 +- doc/api.md | 68 +++++++++++++++++----- lib/cli-data.js | 4 ++ lib/cli-view.js | 31 ++++++++++ lib/local-web-server.js | 148 +++++++++++++++++++++--------------------------- 5 files changed, 156 insertions(+), 97 deletions(-) create mode 100644 lib/cli-view.js diff --git a/README.md b/README.md index 5f1ceb3..1b8a224 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.*** # local-web-server -At its core, local-web-server is an application shell for building a specialised command-line web server to support productive Web Platform engineers. It comes bundled with a middleware stack covering common requirements but any arbitrary stack can be specified from the command line or config. +At its core, local-web-server is an application shell for building a specialised command-line web server to support productive Web Platform engineers. When combined with built-in and custom features it's in intended to by a powerful tool in helping build and debug Web applications. It comes bundled with a middleware stack covering common requirements but any arbitrary stack can be specified from the command line or config. Being an npm module, it is trivial is bundle and distribute/deploy with your web application. diff --git a/doc/api.md b/doc/api.md index fe731a8..b476b8d 100644 --- a/doc/api.md +++ b/doc/api.md @@ -2,29 +2,71 @@ * [local-web-server](#module_local-web-server) - * [LocalWebServer](#exp_module_local-web-server--LocalWebServer) ⇐ [middleware-stack](#module_middleware-stack) ⏏ + * [LocalWebServer](#exp_module_local-web-server--LocalWebServer) ⇐ module:middleware-stack ⏏ + * [new LocalWebServer([options])](#new_module_local-web-server--LocalWebServer_new) * _instance_ - * [.add(middleware)](#) ↩︎ + * [.view](#module_local-web-server--LocalWebServer.LocalWebServer+view) : View + * [.features](#module_local-web-server--LocalWebServer.LocalWebServer+features) : Array.<Feature> + * [.options](#module_local-web-server--LocalWebServer.LocalWebServer+options) : object + * [.server](#module_local-web-server--LocalWebServer+server) : Server + * [.getApplication()](#module_local-web-server--LocalWebServer+getApplication) ⇒ function + * [.getServer()](#module_local-web-server--LocalWebServer+getServer) ⇒ Server * _inner_ - * [~collectUserOptions()](#module_local-web-server--LocalWebServer..collectUserOptions) + * [~loadStack()](#module_local-web-server--LocalWebServer..loadStack) ⇒ object -### LocalWebServer ⇐ [middleware-stack](#module_middleware-stack) ⏏ +### LocalWebServer ⇐ module:middleware-stack ⏏ **Kind**: Exported class -**Extends:** [middleware-stack](#module_middleware-stack) - +**Extends:** module:middleware-stack + -#### localWebServer.add(middleware) ↩︎ -**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) -**Chainable** +#### new LocalWebServer([options]) **Params** -- middleware [middleware](#module_middleware-stack--MiddlewareStack..middleware) +- [options] object - Server options + - .port} number - Port + - .stack} Array.<string> | Array.<Features> - Port + + + +#### localWebServer.view : View +Current view. + +**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + + +#### localWebServer.features : Array.<Feature> +Loaded feature modules + +**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + + +#### localWebServer.options : object +Config + +**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + - +#### localWebServer.server : Server +Node.js server + +**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + + +#### localWebServer.getApplication() ⇒ function +Returns a middleware application suitable for passing to `http.createServer`. The application is a function with three args (req, res and next) which can be created by express, Koa or hand-rolled. + +**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + + +#### localWebServer.getServer() ⇒ Server +Returns a listening server which processes requests using the middleware supplied. + +**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + -#### LocalWebServer~collectUserOptions() -Return default, stored and command-line options combined +#### LocalWebServer~loadStack() ⇒ object +Loads a module by either path or name. **Kind**: inner method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) diff --git a/lib/cli-data.js b/lib/cli-data.js index bbe4605..8385504 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -24,6 +24,10 @@ exports.optionDefinitions = [ description: 'Print these usage instructions.', group: 'misc' }, { + name: 'view', type: String, + description: 'Custom view', group: 'misc' + }, + { name: 'config', type: Boolean, description: 'Print the stored config.', group: 'misc' }, diff --git a/lib/cli-view.js b/lib/cli-view.js new file mode 100644 index 0000000..21fabaa --- /dev/null +++ b/lib/cli-view.js @@ -0,0 +1,31 @@ +class CliView { + constructor (localWebServer) { + this.options = localWebServer.options + } + info (key, value) { + if (key && value) { + const ansi = require('ansi-escape-sequences') + const tableLayout = require('table-layout') + const output = tableLayout({ key: ansi.format(key, 'bold'), value: value}, { + padding: { left: '', right: ' ' }, + columns: [ + { name: 'key', width: 18 }, + { name: 'value', nowrap: true } + ] + }) + process.stderr.write(output) + } else { + console.error(key) + } + } + verbose (key, value) { + if (this.options.verbose) { + this.info(key, value) + } + } + error (msg) { + console.error(ansi.format(msg, 'red')) + } +} + +module.exports = CliView diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 7672894..f43b21e 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -9,37 +9,6 @@ const ansi = require('ansi-escape-sequences') * @module local-web-server */ - -class CliView { - constructor (options) { - this.options = options || {} - } - show (key, value) { - if (key && value) { - const ansi = require('ansi-escape-sequences') - const tableLayout = require('table-layout') - const output = tableLayout({ key: ansi.format(key, 'bold'), value: value}, { - padding: { left: '', right: ' ' }, - columns: [ - { name: 'key', width: 18 }, - { name: 'value', nowrap: true } - ] - }) - process.stderr.write(output) - } else { - console.error(key) - } - } - verbose (key, value) { - if (this.options.verbose) { - this.show(key, value) - } - } - error (msg) { - console.error(ansi.format(msg, 'red')) - } -} - /** * @alias module:local-web-server * @extends module:middleware-stack @@ -55,9 +24,14 @@ class LocalWebServer { initOptions = initOptions || {} const commandLineArgs = require('command-line-args') const commandLineUsage = require('command-line-usage') + const CliView = require('./cli-view') const cli = require('../lib/cli-data') - this.view = new CliView() + /** + * Current view. + * @type {View} + */ + this.view = new CliView(this) /* get stored config */ const loadConfig = require('config-master') @@ -66,11 +40,14 @@ class LocalWebServer { /* read the config and command-line for feature paths */ const featurePaths = parseFeaturePaths(initOptions.stack || stored.stack) - /* load features and build the middleware stack */ - const features = this._buildFeatureStack(featurePaths) + /** + * Loaded feature modules + * @type {Feature[]} + */ + this.features = this._buildFeatureStack(featurePaths) /* gather feature optionDefinitions and parse the command line */ - const featureOptionDefinitions = features + const featureOptionDefinitions = this.features .filter(mw => mw.optionDefinitions) .map(mw => mw.optionDefinitions()) .reduce(flatten, []) @@ -95,7 +72,7 @@ class LocalWebServer { return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` }).join('\n')) } - this.view.show(usage) + this.view.info(usage) process.exit(1) } } @@ -110,41 +87,39 @@ class LocalWebServer { options.misc ) - this.view.options.verbose = options.verbose - /** * Config * @type {object} */ this.options = options - this.features = features - - features - .filter(mw => mw.on) - .forEach(mw => { - mw.on('verbose', this.view.verbose.bind(this.view)) - mw.on('debug', this.view.verbose.bind(this.view)) - }) + if (options.view) { + const View = loadModule(options.view) + this.view = new View(this) + } /* --config */ if (options.config) { - this.view.show(JSON.stringify(options, null, ' ')) + this.view.info(JSON.stringify(options, null, ' ')) process.exit(0) /* --version */ } else if (options.version) { const pkg = require(path.resolve(__dirname, '..', 'package.json')) - this.view.show(pkg.version) + this.view.info(pkg.version) process.exit(0) /* --help */ } else if (options.help) { - this.view.show(usage) + this.view.info(usage) process.exit(0) } } + /** + * Returns a middleware application suitable for passing to `http.createServer`. The application is a function with three args (req, res and next) which can be created by express, Koa or hand-rolled. + * @returns {function} + */ getApplication () { const Koa = require('koa') const app = new Koa() @@ -153,7 +128,7 @@ class LocalWebServer { const middlewareStack = this.features .filter(mw => mw.middleware) - .map(mw => mw.middleware(this.options)) + .map(mw => mw.middleware(this.options, this)) .reduce(flatten, []) .filter(mw => mw) .map(convert) @@ -162,9 +137,13 @@ class LocalWebServer { app.on('error', err => { console.error(ansi.format(err.stack, 'red')) }) - return app + return app.callback() } + /** + * Returns a listening server which processes requests using the middleware supplied. + * @returns {Server} + */ getServer (onListening) { const app = this.getApplication() const options = this.options @@ -186,11 +165,11 @@ class LocalWebServer { } const https = require('https') - server = https.createServer(serverOptions, app.callback()) + server = https.createServer(serverOptions, app) server.isHttps = true } else { const http = require('http') - server = http.createServer(app.callback()) + server = http.createServer(app) } server.listen(options.port) @@ -200,10 +179,15 @@ class LocalWebServer { const ipList = getIPList() .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) .join(', ') - this.view.show('Serving at', ansi.format(ipList)) + this.view.info('Serving at', ansi.format(ipList)) }) } + /** + * Node.js server + * @type {Server} + */ + this.server = server return server } @@ -211,32 +195,25 @@ class LocalWebServer { return featurePaths .map(featurePath => loadStack(featurePath)) .map(Feature => new Feature()) - .map(module => { - if (module.stack) { - const featureStack = module.stack() + .map(feature => { + if (feature.stack) { + const featureStack = feature.stack() .map(Feature => new Feature()) - .map(feature => { - if (feature.on) { - feature.on('verbose', this.view.verbose.bind(this.view)) - feature.on('debug', this.view.verbose.bind(this.view)) - } - return feature - }) - - module.optionDefinitions = function () { + + feature.optionDefinitions = function () { return featureStack .map(feature => feature.optionDefinitions && feature.optionDefinitions()) .filter(definitions => definitions) .reduce(flatten, []) } - module.middleware = function (options) { + feature.middleware = function (options, view) { return featureStack - .map(feature => feature.middleware(options)) + .map(feature => feature.middleware(options, view)) .reduce(flatten, []) .filter(mw => mw) } } - return module + return feature }) } } @@ -246,8 +223,26 @@ class LocalWebServer { * @returns {object} */ function loadStack (modulePath) { - let module + const isModule = module => module.prototype && (module.prototype.middleware || module.prototype.stack) if (isModule(modulePath)) return modulePath + const module = loadModule(modulePath) + if (module) { + if (!isModule(module)) { + const insp = require('util').inspect(module, { depth: 3, colors: true }) + const msg = `Not valid Middleware at: ${insp}` + console.error(msg) + process.exit(1) + } + } else { + const msg = `No module found at: \n${tried.join('\n')}` + console.error(msg) + process.exit(1) + } + return module +} + +function loadModule (modulePath) { + let module const tried = [] if (modulePath) { try { @@ -268,22 +263,9 @@ function loadStack (modulePath) { } } } - if (module) { - if (!isModule(module)) { - const insp = require('util').inspect(module, { depth: 3, colors: true }) - const msg = `Not valid Middleware at: ${insp}` - tool.halt(new Error(msg)) - } - } else { - const msg = `No module found at: \n${tried.join('\n')}` - tool.halt(new Error(msg)) - } return module } -function isModule (module) { - return module.prototype && (module.prototype.middleware || module.prototype.stack) -} function getIPList () { const flatten = require('reduce-flatten') From 9d3484fed68fab1831eb1db739f6253fdeb1b45e Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 17 Jul 2016 17:05:21 +0100 Subject: [PATCH 043/136] server now created on contruction, before views intantiated.. refactor view API --- bin/cli.js | 3 +- doc/api.md | 18 ++++---- doc/visualisation.md | 2 +- lib/cli-view.js | 65 +++++++++++++++++----------- lib/local-web-server.js | 111 ++++++++++++++++++++++++++---------------------- test/test.js | 12 +++--- 6 files changed, 119 insertions(+), 92 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 35c5a3c..019ce92 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,5 +1,4 @@ #!/usr/bin/env node 'use strict' const LocalWebServer = require('../') -const ws = new LocalWebServer() -ws.getServer() +new LocalWebServer() diff --git a/doc/api.md b/doc/api.md index b476b8d..4b4ccf0 100644 --- a/doc/api.md +++ b/doc/api.md @@ -5,10 +5,10 @@ * [LocalWebServer](#exp_module_local-web-server--LocalWebServer) ⇐ module:middleware-stack ⏏ * [new LocalWebServer([options])](#new_module_local-web-server--LocalWebServer_new) * _instance_ - * [.view](#module_local-web-server--LocalWebServer.LocalWebServer+view) : View * [.features](#module_local-web-server--LocalWebServer.LocalWebServer+features) : Array.<Feature> * [.options](#module_local-web-server--LocalWebServer.LocalWebServer+options) : object - * [.server](#module_local-web-server--LocalWebServer+server) : Server + * [.view](#module_local-web-server--LocalWebServer.LocalWebServer+view) : View + * [.server](#module_local-web-server--LocalWebServer.LocalWebServer+server) : Server * [.getApplication()](#module_local-web-server--LocalWebServer+getApplication) ⇒ function * [.getServer()](#module_local-web-server--LocalWebServer+getServer) ⇒ Server * _inner_ @@ -28,12 +28,6 @@ - .port} number - Port - .stack} Array.<string> | Array.<Features> - Port - - -#### localWebServer.view : View -Current view. - -**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) #### localWebServer.features : Array.<Feature> @@ -46,7 +40,13 @@ Loaded feature modules Config **Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - + + +#### localWebServer.view : View +Current view. + +**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) + #### localWebServer.server : Server Node.js server diff --git a/doc/visualisation.md b/doc/visualisation.md index c621023..c31ce9d 100644 --- a/doc/visualisation.md +++ b/doc/visualisation.md @@ -13,7 +13,7 @@ Then, start the server, outputting `combined` format logs to disk: $ ws -f combined > web.log ``` -In a separate tab, point goaccess at `web.log` and it will display statistics in real time: +In a separate terminal, point goaccess at `web.log` and it will display statistics in real time: ``` $ goaccess -p ~/.goaccessrc -f web.log diff --git a/lib/cli-view.js b/lib/cli-view.js index 21fabaa..07503db 100644 --- a/lib/cli-view.js +++ b/lib/cli-view.js @@ -1,31 +1,48 @@ +'use strict' + class CliView { constructor (localWebServer) { - this.options = localWebServer.options + this.localWebServer = localWebServer } - info (key, value) { - if (key && value) { - const ansi = require('ansi-escape-sequences') - const tableLayout = require('table-layout') - const output = tableLayout({ key: ansi.format(key, 'bold'), value: value}, { - padding: { left: '', right: ' ' }, - columns: [ - { name: 'key', width: 18 }, - { name: 'value', nowrap: true } - ] - }) - process.stderr.write(output) - } else { - console.error(key) - } - } - verbose (key, value) { - if (this.options.verbose) { - this.info(key, value) - } - } - error (msg) { - console.error(ansi.format(msg, 'red')) + write (msg) { + const writeToStdout = [ 'log', 'info' ] + Object.keys(msg).forEach(key => { + if (writeToStdout.includes(key)) { + console.log(msg[key]) + } else if (key === 'config' && msg.config && this.localWebServer.options.verbose) { + printLine(msg.config) + } else if (key === 'error') { + const ansi = require('ansi-escape-sequences') + console.error(ansi.format(msg.error, 'red')) + } + }) } } module.exports = CliView + +function printLine (config) { + const output = objectToTable(config) + process.stderr.write(output) +} + +function objectToTable (object) { + const ansi = require('ansi-escape-sequences') + const tableLayout = require('table-layout') + const t = require('typical') + + const data = Object.keys(object).map(key => { + if (t.isObject(object[key])) { + return { key: ansi.format(key, 'bold'), value: objectToTable(object[key]) } + } else { + return { key: ansi.format(key, 'bold'), value: object[key] } + } + }) + return tableLayout(data, { + padding: { left: '', right: ' ' }, + columns: [ + // { name: 'key', width: 18 }, + // { name: 'value', nowrap: true } + ] + }) +} diff --git a/lib/local-web-server.js b/lib/local-web-server.js index f43b21e..4f8c23c 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -22,17 +22,10 @@ class LocalWebServer { */ constructor (initOptions) { initOptions = initOptions || {} - const commandLineArgs = require('command-line-args') const commandLineUsage = require('command-line-usage') const CliView = require('./cli-view') const cli = require('../lib/cli-data') - /** - * Current view. - * @type {View} - */ - this.view = new CliView(this) - /* get stored config */ const loadConfig = require('config-master') const stored = loadConfig('local-web-server') @@ -47,35 +40,10 @@ class LocalWebServer { this.features = this._buildFeatureStack(featurePaths) /* gather feature optionDefinitions and parse the command line */ - const featureOptionDefinitions = this.features - .filter(mw => mw.optionDefinitions) - .map(mw => mw.optionDefinitions()) - .reduce(flatten, []) - .filter(def => def) - .map(def => { - def.group = 'middleware' - return def - }) - + const featureOptionDefinitions = gatherOptionDefinitions(this.features) const usage = commandLineUsage(cli.usage(featureOptionDefinitions)) - - let options = {} const allOptionDefinitions = cli.optionDefinitions.concat(featureOptionDefinitions) - if (!initOptions.testMode) { - try { - options = commandLineArgs(allOptionDefinitions) - } catch (err) { - this.view.error(err.toString()) - if (err.name === 'DUPLICATE_NAME') { - this.view.error('\nOption Definitions:') - this.view.error(allOptionDefinitions.map(def => { - return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` - }).join('\n')) - } - this.view.info(usage) - process.exit(1) - } - } + let options = initOptions.testMode ? {} : parseCommandLineOptions(allOptionDefinitions, this.view) /* combine in stored config */ options = Object.assign( @@ -93,26 +61,41 @@ class LocalWebServer { */ this.options = options - if (options.view) { - const View = loadModule(options.view) - this.view = new View(this) - } + /** + * Current view. + * @type {View} + */ + this.view = null /* --config */ if (options.config) { - this.view.info(JSON.stringify(options, null, ' ')) + console.error(JSON.stringify(options, null, ' ')) process.exit(0) /* --version */ } else if (options.version) { const pkg = require(path.resolve(__dirname, '..', 'package.json')) - this.view.info(pkg.version) + console.error(pkg.version) process.exit(0) /* --help */ } else if (options.help) { - this.view.info(usage) + console.error(usage) process.exit(0) + + } else { + /** + * Node.js server + * @type {Server} + */ + this.server = this.getServer() + + if (options.view) { + const View = loadModule(options.view) + this.view = new View(this) + } else { + this.view = new CliView(this) + } } } @@ -144,7 +127,7 @@ class LocalWebServer { * Returns a listening server which processes requests using the middleware supplied. * @returns {Server} */ - getServer (onListening) { + getServer () { const app = this.getApplication() const options = this.options @@ -173,21 +156,18 @@ class LocalWebServer { } server.listen(options.port) - if (onListening) server.on('listening', onListening) + // if (onListening) server.on('listening', onListening) + + /* on server-up message */ if (!options.testMode) { server.on('listening', () => { const ipList = getIPList() .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) .join(', ') - this.view.info('Serving at', ansi.format(ipList)) + console.error('Serving at', ansi.format(ipList)) }) } - /** - * Node.js server - * @type {Server} - */ - this.server = server return server } @@ -234,7 +214,7 @@ function loadStack (modulePath) { process.exit(1) } } else { - const msg = `No module found at: \n${tried.join('\n')}` + const msg = `No module found for: ${modulePath}` console.error(msg) process.exit(1) } @@ -299,4 +279,35 @@ function parseFeaturePaths (configStack) { return featurePaths } +function gatherOptionDefinitions (features) { + return features + .filter(mw => mw.optionDefinitions) + .map(mw => mw.optionDefinitions()) + .reduce(flatten, []) + .filter(def => def) + .map(def => { + def.group = 'middleware' + return def + }) +} + +function parseCommandLineOptions (allOptionDefinitions) { + const commandLineArgs = require('command-line-args') + try { + return commandLineArgs(allOptionDefinitions) + } catch (err) { + console.error(err) + + /* handle duplicate option names */ + if (err.name === 'DUPLICATE_NAME') { + console.error('\nOption Definitions:') + console.error(allOptionDefinitions.map(def => { + return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` + }).join('\n')) + } + console.error(usage) + process.exit(1) + } +} + module.exports = LocalWebServer diff --git a/test/test.js b/test/test.js index 68a6d6d..b1ba7ea 100644 --- a/test/test.js +++ b/test/test.js @@ -12,13 +12,13 @@ test('stack', function (t) { port: 8100, testMode: true }) - const server = ws.getServer(() => { + ws.server.on('listening', () => { return request('http://localhost:8100/') .then(c.checkResponse(t, 200, /1234512345/)) - .then(server.close.bind(server)) + .then(ws.server.close.bind(ws.server)) .catch(err => { t.fail(err.message) - server.close() + ws.server.close() }) }) }) @@ -34,13 +34,13 @@ test('https', function (t) { const url = require('url') const reqOptions = url.parse('https://localhost:8100/') reqOptions.rejectUnauthorized = false - const server = ws.getServer(() => { + ws.server.on('listening', () => { return request(reqOptions) .then(c.checkResponse(t, 200, /1234512345/)) - .then(server.close.bind(server)) + .then(ws.server.close.bind(ws.server)) .catch(err => { t.fail(err.message) - server.close() + ws.server.close() }) }) }) From a9af4fa9598652d14ec6364ffbc16db3e5e9e881 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 18 Jul 2016 17:33:07 +0100 Subject: [PATCH 044/136] middleware docs --- lib/local-web-server.js | 1 - lib/middleware.js | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 lib/middleware.js diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 4f8c23c..d9f21fd 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -246,7 +246,6 @@ function loadModule (modulePath) { return module } - function getIPList () { const flatten = require('reduce-flatten') const os = require('os') diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 0000000..6b654b7 --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,14 @@ +'use strict' + +class Middleware { + /** + * Return one or more options definitions to collect command-line input + * @returns {OptionDefinition|OptionDefinition[]} + */ + optionDefinitions () {} + + /** + * Return one of more middleware functions with three args (req, res and next). Can be created by express, Koa or hand-rolled. + */ + middleware (localWebServer) {} +} From e81b55642fd9ac91ade458739160685bda3c6634 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 20 Jul 2016 22:16:58 +0100 Subject: [PATCH 045/136] 2.0.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 347e776..c57890b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "1.2.6", + "version": "2.0.0-0", "description": "A simple web-server for productive front-end development", "bin": { "ws": "./bin/cli.js" From aa3851f5f8ac25ae3db67d56325bc44b24c3bb96 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 20 Jul 2016 22:19:31 +0100 Subject: [PATCH 046/136] new package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c57890b..66a3bab 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "local-web-server", + "name": "lws", "version": "2.0.0-0", "description": "A simple web-server for productive front-end development", "bin": { From 5761dd297ae4692742da9914abb900eb5394b10f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 18 Oct 2016 21:43:43 +0100 Subject: [PATCH 047/136] upgrade deps --- lib/cli-data.js | 4 ++++ lib/cli-view.js | 8 ++++++++ lib/local-web-server.js | 17 +++++++++++++++++ package.json | 10 +++++----- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/cli-data.js b/lib/cli-data.js index 8385504..f232d25 100644 --- a/lib/cli-data.js +++ b/lib/cli-data.js @@ -32,6 +32,10 @@ exports.optionDefinitions = [ description: 'Print the stored config.', group: 'misc' }, { + name: 'config-file', type: String, + description: 'Config file to use', group: 'misc' + }, + { name: 'verbose', type: Boolean, alias: 'v', description: 'Verbose output.', group: 'misc' }, diff --git a/lib/cli-view.js b/lib/cli-view.js index 07503db..eaba5a4 100644 --- a/lib/cli-view.js +++ b/lib/cli-view.js @@ -4,6 +4,11 @@ class CliView { constructor (localWebServer) { this.localWebServer = localWebServer } + /** + * @example + * { log: 'whatever' } + * { config: { static: { root: 1, hidden: 2 } } } + */ write (msg) { const writeToStdout = [ 'log', 'info' ] Object.keys(msg).forEach(key => { @@ -26,6 +31,9 @@ function printLine (config) { process.stderr.write(output) } +/** + * create a nested table for deep object trees + */ function objectToTable (object) { const ansi = require('ansi-escape-sequences') const tableLayout = require('table-layout') diff --git a/lib/local-web-server.js b/lib/local-web-server.js index d9f21fd..31f095e 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,5 +1,22 @@ #!/usr/bin/env node 'use strict' + +// /Users/lloydb/Documents/lws/local-web-server/lib/local-web-server.js:307 +// console.error(usage) +// ^ +// +// ReferenceError: usage is not defined +// at parseCommandLineOptions (/Users/lloydb/Documents/lws/local-web-server/lib/local-web-server.js:307:19) +// at new LocalWebServer (/Users/lloydb/Documents/lws/local-web-server/lib/local-web-server.js:46:47) +// at Object. (/Users/lloydb/Documents/lws/local-web-server/bin/cli.js:4:1) +// at Module._compile (module.js:556:32) +// at Object.Module._extensions..js (module.js:565:10) +// at Module.load (module.js:473:32) +// at tryModuleLoad (module.js:432:12) +// at Function.Module._load (module.js:424:3) +// at Module.runMain (module.js:590:10) +// at run (bootstrap_node.js:394:7) + const path = require('path') const flatten = require('reduce-flatten') const arrayify = require('array-back') diff --git a/package.json b/package.json index 66a3bab..29bb056 100644 --- a/package.json +++ b/package.json @@ -39,23 +39,23 @@ "dependencies": { "ansi-escape-sequences": "^2.2.2", "array-back": "^1.0.3", - "command-line-args": "^3.0.0", + "command-line-args": "^3.0.1", "command-line-usage": "^3.0.3", - "config-master": "^2.0.3", + "config-master": "^2.0.4", "koa": "^2.0.0", "koa-compose": "^3.1.0", "koa-convert": "^1.2.0", "local-web-server-default-stack": "github:local-web-server/default-stack", "reduce-flatten": "^1.0.1", "table-layout": "~0.2.2", - "typical": "^2.4.2", + "typical": "^2.6.0", "walk-back": "^2.0.1" }, "devDependencies": { - "jsdoc-to-markdown": "^1.3.6", + "jsdoc-to-markdown": "^1.3.7", "koa-cache-control": "^1.0.0", "koa-livereload": "~0.2.0", - "req-then": "~0.3.3", + "req-then": "~0.5.0", "tape": "^4.6.0" } } From f9b3a69f19a913fdf784b38e0ad5d6ce4e1a8950 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 3 Nov 2016 14:27:04 +0000 Subject: [PATCH 048/136] upgrade deps --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 29bb056..0b18782 100644 --- a/package.json +++ b/package.json @@ -37,25 +37,25 @@ "repository": "https://github.com/75lb/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "ansi-escape-sequences": "^2.2.2", + "ansi-escape-sequences": "^3.0.0", "array-back": "^1.0.3", - "command-line-args": "^3.0.1", - "command-line-usage": "^3.0.3", - "config-master": "^2.0.4", - "koa": "^2.0.0", + "command-line-args": "^3.0.2", + "command-line-usage": "^3.0.5", + "config-master": "^2.1.0-0", + "koa": "next", "koa-compose": "^3.1.0", "koa-convert": "^1.2.0", "local-web-server-default-stack": "github:local-web-server/default-stack", "reduce-flatten": "^1.0.1", - "table-layout": "~0.2.2", + "table-layout": "~0.2.3", "typical": "^2.6.0", "walk-back": "^2.0.1" }, "devDependencies": { - "jsdoc-to-markdown": "^1.3.7", + "jsdoc-to-markdown": "^2.0.1", "koa-cache-control": "^1.0.0", "koa-livereload": "~0.2.0", - "req-then": "~0.5.0", - "tape": "^4.6.0" + "req-then": "~0.5.1", + "tape": "^4.6.2" } } From 698864299a61401d5ce2b995869217b80b131ea4 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 30 Jan 2017 22:59:50 +0000 Subject: [PATCH 049/136] refactor.. feature.ready() method.. --stack paths now scan ws modules --- lib/feature.js | 44 +++++++++++++++++++++++ lib/local-web-server.js | 93 +++++++++++++++++++++++++++---------------------- lib/middleware.js | 14 -------- package.json | 10 +++--- 4 files changed, 100 insertions(+), 61 deletions(-) create mode 100644 lib/feature.js delete mode 100644 lib/middleware.js diff --git a/lib/feature.js b/lib/feature.js new file mode 100644 index 0000000..256bd0a --- /dev/null +++ b/lib/feature.js @@ -0,0 +1,44 @@ +'use strict' + +/** + * Feature interface. + */ +class Feature { + constructor (localWebServer) {} + + /** + * Return one or more options definitions to collect command-line input + * @returns {OptionDefinition|OptionDefinition[]} + */ + optionDefinitions () {} + + /** + * Return one of more middleware functions with three args (req, res and next). Can be created by express, Koa or hand-rolled. + */ + middleware (options) {} + + expandStack () { + const flatten = require('reduce-flatten') + + if (this.stack) { + const featureStack = this.stack() + .map(Feature => new Feature()) + + this.optionDefinitions = function () { + return featureStack + .map(feature => feature.optionDefinitions && feature.optionDefinitions()) + .filter(definitions => definitions) + .reduce(flatten, []) + } + this.middleware = function (options, view) { + return featureStack + .map(feature => feature.middleware(options, view)) + .reduce(flatten, []) + .filter(mw => mw) + } + } + return this + } +} + +module.exports = Feature diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 31f095e..5f4c9fb 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,22 +1,6 @@ #!/usr/bin/env node 'use strict' -// /Users/lloydb/Documents/lws/local-web-server/lib/local-web-server.js:307 -// console.error(usage) -// ^ -// -// ReferenceError: usage is not defined -// at parseCommandLineOptions (/Users/lloydb/Documents/lws/local-web-server/lib/local-web-server.js:307:19) -// at new LocalWebServer (/Users/lloydb/Documents/lws/local-web-server/lib/local-web-server.js:46:47) -// at Object. (/Users/lloydb/Documents/lws/local-web-server/bin/cli.js:4:1) -// at Module._compile (module.js:556:32) -// at Object.Module._extensions..js (module.js:565:10) -// at Module.load (module.js:473:32) -// at tryModuleLoad (module.js:432:12) -// at Function.Module._load (module.js:424:3) -// at Module.runMain (module.js:590:10) -// at run (bootstrap_node.js:394:7) - const path = require('path') const flatten = require('reduce-flatten') const arrayify = require('array-back') @@ -113,6 +97,12 @@ class LocalWebServer { } else { this.view = new CliView(this) } + + for (const feature of this.features) { + if (feature.ready) { + feature.ready(this) + } + } } } @@ -188,39 +178,25 @@ class LocalWebServer { return server } + /** + * Returns an array of Feature instances, given their module paths/names. + * @return {feature[]} + */ _buildFeatureStack (featurePaths) { + const FeatureBase = require('./feature') return featurePaths - .map(featurePath => loadStack(featurePath)) - .map(Feature => new Feature()) - .map(feature => { - if (feature.stack) { - const featureStack = feature.stack() - .map(Feature => new Feature()) - - feature.optionDefinitions = function () { - return featureStack - .map(feature => feature.optionDefinitions && feature.optionDefinitions()) - .filter(definitions => definitions) - .reduce(flatten, []) - } - feature.middleware = function (options, view) { - return featureStack - .map(feature => feature.middleware(options, view)) - .reduce(flatten, []) - .filter(mw => mw) - } - } - return feature - }) + .map(featurePath => loadFeature(featurePath)) + .map(Feature => new Feature(this)) + .map(feature => FeatureBase.prototype.expandStack.call(feature)) } } /** - * Loads a module by either path or name. - * @returns {object} + * Load a module and verify it's of the correct type + * @returns {Feature} */ -function loadStack (modulePath) { - const isModule = module => module.prototype && (module.prototype.middleware || module.prototype.stack) +function loadFeature (modulePath) { + const isModule = module => module.prototype && (module.prototype.middleware || module.prototype.stack || module.prototype.ready) if (isModule(modulePath)) return modulePath const module = loadModule(modulePath) if (module) { @@ -238,6 +214,14 @@ function loadStack (modulePath) { return module } +/** + * Returns a module, loaded by the first to succeed from + * - direct path + * - 'node_modules/local-web-server-' + path, from current folder upward + * - 'node_modules/' + path, from current folder upward + * - also search local-web-server project node_modules? (e.g. to search for a feature module without need installing it locally) + * @returns {object} + */ function loadModule (modulePath) { let module const tried = [] @@ -256,6 +240,16 @@ function loadModule (modulePath) { tried.push(modulePath) if (foundPath2) { module = require(foundPath2) + } else { + const foundPath3 = walkBack(path.resolve(__filename, '..'), path.join('node_modules', 'local-web-server-' + modulePath)) + if (foundPath3) { + return require(foundPath3) + } else { + const foundPath4 = walkBack(path.resolve(__filename, '..'), path.join('node_modules', modulePath)) + if (foundPath4) { + return require(foundPath4) + } + } } } } @@ -263,6 +257,21 @@ function loadModule (modulePath) { return module } +/** + * Returns an array of available IPv4 network interfaces + * @example + * [ { address: 'mbp.local' }, + * { address: '127.0.0.1', + * netmask: '255.0.0.0', + * family: 'IPv4', + * mac: '00:00:00:00:00:00', + * internal: true }, + * { address: '192.168.1.86', + * netmask: '255.255.255.0', + * family: 'IPv4', + * mac: 'd0:a6:37:e9:86:49', + * internal: false } ] + */ function getIPList () { const flatten = require('reduce-flatten') const os = require('os') diff --git a/lib/middleware.js b/lib/middleware.js deleted file mode 100644 index 6b654b7..0000000 --- a/lib/middleware.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict' - -class Middleware { - /** - * Return one or more options definitions to collect command-line input - * @returns {OptionDefinition|OptionDefinition[]} - */ - optionDefinitions () {} - - /** - * Return one of more middleware functions with three args (req, res and next). Can be created by express, Koa or hand-rolled. - */ - middleware (localWebServer) {} -} diff --git a/package.json b/package.json index 0b18782..8fdfb78 100644 --- a/package.json +++ b/package.json @@ -38,16 +38,16 @@ "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { "ansi-escape-sequences": "^3.0.0", - "array-back": "^1.0.3", - "command-line-args": "^3.0.2", - "command-line-usage": "^3.0.5", + "array-back": "^1.0.4", + "command-line-args": "^3.0.5", + "command-line-usage": "^4.0.0", "config-master": "^2.1.0-0", "koa": "next", "koa-compose": "^3.1.0", "koa-convert": "^1.2.0", "local-web-server-default-stack": "github:local-web-server/default-stack", "reduce-flatten": "^1.0.1", - "table-layout": "~0.2.3", + "table-layout": "~0.4.0", "typical": "^2.6.0", "walk-back": "^2.0.1" }, @@ -56,6 +56,6 @@ "koa-cache-control": "^1.0.0", "koa-livereload": "~0.2.0", "req-then": "~0.5.1", - "tape": "^4.6.2" + "tape": "^4.6.3" } } From 86b2b52291c460fd5d5f9eb79cafc38ef7b620a7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 8 Feb 2017 18:37:53 +0000 Subject: [PATCH 050/136] ensure non 'MODULE_NOT_FOUND' exceptions are thrown by loadModule --- lib/local-web-server.js | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 5f4c9fb..d2735bc 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -230,6 +230,9 @@ function loadModule (modulePath) { tried.push(path.resolve(modulePath)) module = require(path.resolve(modulePath)) } catch (err) { + if (!(err && err.code === 'MODULE_NOT_FOUND')) { + throw err + } const walkBack = require('walk-back') const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath)) tried.push('local-web-server-' + modulePath) diff --git a/package.json b/package.json index 8fdfb78..9c09fac 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "dependencies": { "ansi-escape-sequences": "^3.0.0", "array-back": "^1.0.4", - "command-line-args": "^3.0.5", + "command-line-args": "^4.0.1", "command-line-usage": "^4.0.0", "config-master": "^2.1.0-0", "koa": "next", From f98e39d6e70a99e6c058345f1e70b4dcbf785345 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 17 Feb 2017 17:48:46 +0000 Subject: [PATCH 051/136] upgrade deps --- lib/feature.js | 3 +++ package.json | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/feature.js b/lib/feature.js index 256bd0a..d3e7198 100644 --- a/lib/feature.js +++ b/lib/feature.js @@ -4,6 +4,9 @@ * Feature interface. */ class Feature { + /** + * localWebServer instance passed to constructor in case feature needs access to http server instance. + */ constructor (localWebServer) {} /** diff --git a/package.json b/package.json index 9c09fac..98d4f76 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "array-back": "^1.0.4", "command-line-args": "^4.0.1", "command-line-usage": "^4.0.0", - "config-master": "^2.1.0-0", + "config-master": "^3.0.0", "koa": "next", "koa-compose": "^3.1.0", "koa-convert": "^1.2.0", @@ -52,7 +52,7 @@ "walk-back": "^2.0.1" }, "devDependencies": { - "jsdoc-to-markdown": "^2.0.1", + "jsdoc-to-markdown": "^3.0.0", "koa-cache-control": "^1.0.0", "koa-livereload": "~0.2.0", "req-then": "~0.5.1", From ce0f1176f6ed5cfcfd0ae4a226bb61aedabf319a Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 17 Feb 2017 17:48:55 +0000 Subject: [PATCH 052/136] 2.0.0-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98d4f76..6722683 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lws", - "version": "2.0.0-0", + "version": "2.0.0-1", "description": "A simple web-server for productive front-end development", "bin": { "ws": "./bin/cli.js" From 66dc199b8b7acad300e23a8bb8e44a6544513e9c Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 17 Feb 2017 17:51:39 +0000 Subject: [PATCH 053/136] fix package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6722683..19c762b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "lws", + "name": "local-web-server", "version": "2.0.0-1", "description": "A simple web-server for productive front-end development", "bin": { From 662cb2efb4c2bfff93618a3ab550a918dc4cb7f5 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 17 Feb 2017 17:51:57 +0000 Subject: [PATCH 054/136] 2.0.0-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19c762b..86358ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-1", + "version": "2.0.0-2", "description": "A simple web-server for productive front-end development", "bin": { "ws": "./bin/cli.js" From f18a07ba5a329e1ce836dcbe8af7d54a6c00becb Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 13 Mar 2017 23:44:11 +0000 Subject: [PATCH 055/136] switch to using lws.. clean up --- .travis.yml | 4 +- LICENSE | 2 +- README.md | 2 +- bin/cli.js | 3 +- lib/cli-data.js | 87 ------------- lib/feature.js | 47 ------- lib/local-web-server.js | 332 +----------------------------------------------- package.json | 26 +--- ssl/127.0.0.1.crt | 20 --- ssl/127.0.0.1.key | 27 ---- 10 files changed, 15 insertions(+), 535 deletions(-) delete mode 100644 lib/cli-data.js delete mode 100644 lib/feature.js delete mode 100644 ssl/127.0.0.1.crt delete mode 100644 ssl/127.0.0.1.key diff --git a/.travis.yml b/.travis.yml index 56c2d58..fb0b4ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ language: node_js node_js: - - 4 - - 5 - - 6 + - 7 diff --git a/LICENSE b/LICENSE index 677f7e8..8f01d24 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-16 Lloyd Brookes <75pound@gmail.com> +Copyright (c) 2013-17 Lloyd Brookes <75pound@gmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1b8a224..cc529ac 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,4 @@ $ npm install -g local-web-server ``` * * * -© 2013-16 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). +© 2013-17 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). diff --git a/bin/cli.js b/bin/cli.js index 019ce92..f8c6095 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,4 +1,5 @@ #!/usr/bin/env node 'use strict' const LocalWebServer = require('../') -new LocalWebServer() +const localWebServer = new LocalWebServer() +localWebServer.start() diff --git a/lib/cli-data.js b/lib/cli-data.js deleted file mode 100644 index f232d25..0000000 --- a/lib/cli-data.js +++ /dev/null @@ -1,87 +0,0 @@ -exports.optionDefinitions = [ - { - name: 'port', alias: 'p', type: Number, defaultOption: true, - description: 'Web server port.', group: 'server' - }, - { - name: 'stack', type: String, multiple: true, - description: 'Feature stack.', group: 'server' - }, - { - name: 'key', type: String, typeLabel: '[underline]{file}', group: 'server', - description: 'SSL key. Supply along with --cert to launch a https server.' - }, - { - name: 'cert', type: String, typeLabel: '[underline]{file}', group: 'server', - description: 'SSL cert. Supply along with --key to launch a https server.' - }, - { - name: 'https', type: Boolean, group: 'server', - description: 'Enable HTTPS using a built-in key and cert, registered to the domain 127.0.0.1.' - }, - { - name: 'help', alias: 'h', type: Boolean, - description: 'Print these usage instructions.', group: 'misc' - }, - { - name: 'view', type: String, - description: 'Custom view', group: 'misc' - }, - { - name: 'config', type: Boolean, - description: 'Print the stored config.', group: 'misc' - }, - { - name: 'config-file', type: String, - description: 'Config file to use', group: 'misc' - }, - { - name: 'verbose', type: Boolean, alias: 'v', - description: 'Verbose output.', group: 'misc' - }, - { - name: 'debug', type: Boolean, - description: 'Very verbose output, intended for debugging.', group: 'misc' - }, - { - name: 'version', type: Boolean, - description: 'Print the version number.', group: 'misc' - } -] - -function usage (middlewareDefinitions) { - return [ - { - header: 'local-web-server', - content: 'A simple web-server for productive front-end development.' - }, - { - header: 'Synopsis', - content: [ - '$ ws [--verbose] [] []', - '$ ws --config', - '$ ws --help' - ] - }, - { - header: 'Server', - optionList: exports.optionDefinitions, - group: 'server' - }, - { - header: 'Middleware', - optionList: middlewareDefinitions, - group: 'middleware' - }, - { - header: 'Misc', - optionList: exports.optionDefinitions, - group: 'misc' - }, - { - content: 'Project home: [underline]{https://github.com/75lb/local-web-server}' - } - ] -} - -exports.usage = usage diff --git a/lib/feature.js b/lib/feature.js deleted file mode 100644 index d3e7198..0000000 --- a/lib/feature.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict' - -/** - * Feature interface. - */ -class Feature { - /** - * localWebServer instance passed to constructor in case feature needs access to http server instance. - */ - constructor (localWebServer) {} - - /** - * Return one or more options definitions to collect command-line input - * @returns {OptionDefinition|OptionDefinition[]} - */ - optionDefinitions () {} - - /** - * Return one of more middleware functions with three args (req, res and next). Can be created by express, Koa or hand-rolled. - */ - middleware (options) {} - - expandStack () { - const flatten = require('reduce-flatten') - - if (this.stack) { - const featureStack = this.stack() - .map(Feature => new Feature()) - - this.optionDefinitions = function () { - return featureStack - .map(feature => feature.optionDefinitions && feature.optionDefinitions()) - .filter(definitions => definitions) - .reduce(flatten, []) - } - this.middleware = function (options, view) { - return featureStack - .map(feature => feature.middleware(options, view)) - .reduce(flatten, []) - .filter(mw => mw) - } - } - return this - } -} - -module.exports = Feature diff --git a/lib/local-web-server.js b/lib/local-web-server.js index d2735bc..c477536 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,10 +1,5 @@ -#!/usr/bin/env node 'use strict' - -const path = require('path') -const flatten = require('reduce-flatten') -const arrayify = require('array-back') -const ansi = require('ansi-escape-sequences') +const Lws = require('lws') /** * @module local-web-server @@ -12,329 +7,12 @@ const ansi = require('ansi-escape-sequences') /** * @alias module:local-web-server - * @extends module:middleware-stack - */ -class LocalWebServer { - - /** - * @param [options] {object} - Server options - * @param [options.port} {number} - Port - * @param [options.stack} {string[]|Features[]} - Port - */ - constructor (initOptions) { - initOptions = initOptions || {} - const commandLineUsage = require('command-line-usage') - const CliView = require('./cli-view') - const cli = require('../lib/cli-data') - - /* get stored config */ - const loadConfig = require('config-master') - const stored = loadConfig('local-web-server') - - /* read the config and command-line for feature paths */ - const featurePaths = parseFeaturePaths(initOptions.stack || stored.stack) - - /** - * Loaded feature modules - * @type {Feature[]} - */ - this.features = this._buildFeatureStack(featurePaths) - - /* gather feature optionDefinitions and parse the command line */ - const featureOptionDefinitions = gatherOptionDefinitions(this.features) - const usage = commandLineUsage(cli.usage(featureOptionDefinitions)) - const allOptionDefinitions = cli.optionDefinitions.concat(featureOptionDefinitions) - let options = initOptions.testMode ? {} : parseCommandLineOptions(allOptionDefinitions, this.view) - - /* combine in stored config */ - options = Object.assign( - { port: 8000 }, - initOptions, - stored, - options.server, - options.middleware, - options.misc - ) - - /** - * Config - * @type {object} - */ - this.options = options - - /** - * Current view. - * @type {View} - */ - this.view = null - - /* --config */ - if (options.config) { - console.error(JSON.stringify(options, null, ' ')) - process.exit(0) - - /* --version */ - } else if (options.version) { - const pkg = require(path.resolve(__dirname, '..', 'package.json')) - console.error(pkg.version) - process.exit(0) - - /* --help */ - } else if (options.help) { - console.error(usage) - process.exit(0) - - } else { - /** - * Node.js server - * @type {Server} - */ - this.server = this.getServer() - - if (options.view) { - const View = loadModule(options.view) - this.view = new View(this) - } else { - this.view = new CliView(this) - } - - for (const feature of this.features) { - if (feature.ready) { - feature.ready(this) - } - } - } - } - - /** - * Returns a middleware application suitable for passing to `http.createServer`. The application is a function with three args (req, res and next) which can be created by express, Koa or hand-rolled. - * @returns {function} - */ - getApplication () { - const Koa = require('koa') - const app = new Koa() - const compose = require('koa-compose') - const convert = require('koa-convert') - - const middlewareStack = this.features - .filter(mw => mw.middleware) - .map(mw => mw.middleware(this.options, this)) - .reduce(flatten, []) - .filter(mw => mw) - .map(convert) - - app.use(compose(middlewareStack)) - app.on('error', err => { - console.error(ansi.format(err.stack, 'red')) - }) - return app.callback() - } - - /** - * Returns a listening server which processes requests using the middleware supplied. - * @returns {Server} - */ - getServer () { - const app = this.getApplication() - const options = this.options - - let key = options.key - let cert = options.cert - - if (options.https && !(key && cert)) { - 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) - server.isHttps = true - } else { - const http = require('http') - server = http.createServer(app) - } - - server.listen(options.port) - // if (onListening) server.on('listening', onListening) - - /* on server-up message */ - if (!options.testMode) { - server.on('listening', () => { - const ipList = getIPList() - .map(iface => `[underline]{${server.isHttps ? 'https' : 'http'}://${iface.address}:${options.port}}`) - .join(', ') - console.error('Serving at', ansi.format(ipList)) - }) - } - - return server - } - - /** - * Returns an array of Feature instances, given their module paths/names. - * @return {feature[]} - */ - _buildFeatureStack (featurePaths) { - const FeatureBase = require('./feature') - return featurePaths - .map(featurePath => loadFeature(featurePath)) - .map(Feature => new Feature(this)) - .map(feature => FeatureBase.prototype.expandStack.call(feature)) - } -} - -/** - * Load a module and verify it's of the correct type - * @returns {Feature} - */ -function loadFeature (modulePath) { - const isModule = module => module.prototype && (module.prototype.middleware || module.prototype.stack || module.prototype.ready) - if (isModule(modulePath)) return modulePath - const module = loadModule(modulePath) - if (module) { - if (!isModule(module)) { - const insp = require('util').inspect(module, { depth: 3, colors: true }) - const msg = `Not valid Middleware at: ${insp}` - console.error(msg) - process.exit(1) - } - } else { - const msg = `No module found for: ${modulePath}` - console.error(msg) - process.exit(1) - } - return module -} - -/** - * Returns a module, loaded by the first to succeed from - * - direct path - * - 'node_modules/local-web-server-' + path, from current folder upward - * - 'node_modules/' + path, from current folder upward - * - also search local-web-server project node_modules? (e.g. to search for a feature module without need installing it locally) - * @returns {object} - */ -function loadModule (modulePath) { - let module - const tried = [] - if (modulePath) { - try { - tried.push(path.resolve(modulePath)) - module = require(path.resolve(modulePath)) - } catch (err) { - if (!(err && err.code === 'MODULE_NOT_FOUND')) { - throw err - } - const walkBack = require('walk-back') - const foundPath = walkBack(process.cwd(), path.join('node_modules', 'local-web-server-' + modulePath)) - tried.push('local-web-server-' + modulePath) - if (foundPath) { - module = require(foundPath) - } else { - const foundPath2 = walkBack(process.cwd(), path.join('node_modules', modulePath)) - tried.push(modulePath) - if (foundPath2) { - module = require(foundPath2) - } else { - const foundPath3 = walkBack(path.resolve(__filename, '..'), path.join('node_modules', 'local-web-server-' + modulePath)) - if (foundPath3) { - return require(foundPath3) - } else { - const foundPath4 = walkBack(path.resolve(__filename, '..'), path.join('node_modules', modulePath)) - if (foundPath4) { - return require(foundPath4) - } - } - } - } - } - } - return module -} - -/** - * Returns an array of available IPv4 network interfaces - * @example - * [ { address: 'mbp.local' }, - * { address: '127.0.0.1', - * netmask: '255.0.0.0', - * family: 'IPv4', - * mac: '00:00:00:00:00:00', - * internal: true }, - * { address: '192.168.1.86', - * netmask: '255.255.255.0', - * family: 'IPv4', - * mac: 'd0:a6:37:e9:86:49', - * internal: false } ] */ -function getIPList () { - const flatten = require('reduce-flatten') - const os = require('os') - - let ipList = Object.keys(os.networkInterfaces()) - .map(key => os.networkInterfaces()[key]) - .reduce(flatten, []) - .filter(iface => iface.family === 'IPv4') - ipList.unshift({ address: os.hostname() }) - return ipList -} - -/* manually scan for any --stack passed, as we may need to display stack options */ -function parseFeaturePaths (configStack) { - const featurePaths = arrayify(configStack) - const featureIndex = process.argv.indexOf('--stack') - if (featureIndex > -1) { - for (var i = featureIndex + 1; i < process.argv.length; i++) { - const featurePath = process.argv[i] - if (/^-/.test(featurePath)) { - break - } else { - featurePaths.push(featurePath) - } - } - } - - /* if the user did not supply a stack, use the default */ - if (!featurePaths.length) featurePaths.push(path.resolve(__dirname, '..', 'node_modules', 'local-web-server-default-stack')) - return featurePaths -} - -function gatherOptionDefinitions (features) { - return features - .filter(mw => mw.optionDefinitions) - .map(mw => mw.optionDefinitions()) - .reduce(flatten, []) - .filter(def => def) - .map(def => { - def.group = 'middleware' - return def +class LocalWebServer extends Lws { + constructor () { + super({ + stack: [ 'log', 'static' ] }) -} - -function parseCommandLineOptions (allOptionDefinitions) { - const commandLineArgs = require('command-line-args') - try { - return commandLineArgs(allOptionDefinitions) - } catch (err) { - console.error(err) - - /* handle duplicate option names */ - if (err.name === 'DUPLICATE_NAME') { - console.error('\nOption Definitions:') - console.error(allOptionDefinitions.map(def => { - return `name: ${def.name}${def.alias ? ', alias: ' + def.alias : ''}` - }).join('\n')) - } - console.error(usage) - process.exit(1) } } diff --git a/package.json b/package.json index 86358ff..23e2efc 100644 --- a/package.json +++ b/package.json @@ -30,32 +30,16 @@ "ssl" ], "scripts": { - "test": "tape test/*.js", + "test": "test-runner test/*.js", "docs": "jsdoc2md -t jsdoc2md/api.hbs -p list lib/*.js > doc/api.md; echo", "cover": "istanbul cover ./node_modules/.bin/tape test/*.js && cat coverage/lcov.info | coveralls && rm -rf coverage; echo" }, "repository": "https://github.com/75lb/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "ansi-escape-sequences": "^3.0.0", - "array-back": "^1.0.4", - "command-line-args": "^4.0.1", - "command-line-usage": "^4.0.0", - "config-master": "^3.0.0", - "koa": "next", - "koa-compose": "^3.1.0", - "koa-convert": "^1.2.0", - "local-web-server-default-stack": "github:local-web-server/default-stack", - "reduce-flatten": "^1.0.1", - "table-layout": "~0.4.0", - "typical": "^2.6.0", - "walk-back": "^2.0.1" + "local-web-server-log": "file:///Users/lloydb/Documents/lws/log", + "local-web-server-static": "file:///Users/lloydb/Documents/lws/static", + "lws": "^0.8.1" }, - "devDependencies": { - "jsdoc-to-markdown": "^3.0.0", - "koa-cache-control": "^1.0.0", - "koa-livereload": "~0.2.0", - "req-then": "~0.5.1", - "tape": "^4.6.3" - } + "devDependencies": {} } diff --git a/ssl/127.0.0.1.crt b/ssl/127.0.0.1.crt deleted file mode 100644 index bb95c77..0000000 --- a/ssl/127.0.0.1.crt +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDLjCCAhYCCQC3MW7xH6DDyTANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJH -QjETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRIwEAYDVQQDEwkxMjcuMC4wLjEwHhcNMTYwMzEwMTAzMTMwWhcN -MTcwMzEwMTAzMTMwWjBZMQswCQYDVQQGEwJHQjETMBEGA1UECBMKU29tZS1TdGF0 -ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwkx -MjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIWz+H5P3P -5/Uixviwbj88y112TBCCdhPizqVb8f7EgTgeIA0Jpqe2+RR9siawwUAX9nqRUB1g -vgLZE4NZS+5ICN3JqkC4EysDS6VtIVf2OAuem3kdKaHSLl4JabsmBprgf2Dtze0i -eX5+Pur5Pi2BEAYNCUKzC4OuVaP//3jNWD/Xp6eHBbC76L03EIGPxytYf5wkITbY -wCjIVQw0Mq+WsV9eJRuLT4bnoeefCK+zPeTEQ6o+3SFkTkhqfsTF83sHvgcy1T4u -7f+GZ9TYiaUi/1OVvfUg2FdGDAlKtVVH/t+pAg0M2hGr7vTClSVOg/qiY3ktEaYW -FvcxJa65DyQNAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAFwrxsqXwA6BTFTvRYi1 -s4tqos8loaZxE4eug96mL7qRvYzhDY+nDluiDEjMapACQOQaGIV+uMraOBk9yCUo -BsYqLcBLUTKBZvIMEmYmlUKxZrtFLVo1y6p7CJM9luwUEpbPRivA/Vofk9zlq9B1 -AeVjDtqK/iZbO05qN18sgp7VPZZc4zRLOYUGfiUfX6r+dvDAPx/NBFM3vAEyYSur -Jqa2CdsiUXo08CytgIaxGgF1DJxLqoA4SZagSUWWcuOlDzLSooNlcW/zfEfQfeMQ -h7SbUtD4IJuKNd0BCeWMyVN7rM91zp9tf7713l+skbo5wIJAsNQAa2o8uRIXLjNX -jy4= ------END CERTIFICATE----- diff --git a/ssl/127.0.0.1.key b/ssl/127.0.0.1.key deleted file mode 100644 index 5746322..0000000 --- a/ssl/127.0.0.1.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAyFs/h+T9z+f1Isb4sG4/PMtddkwQgnYT4s6lW/H+xIE4HiAN -CaantvkUfbImsMFAF/Z6kVAdYL4C2RODWUvuSAjdyapAuBMrA0ulbSFX9jgLnpt5 -HSmh0i5eCWm7Jgaa4H9g7c3tInl+fj7q+T4tgRAGDQlCswuDrlWj//94zVg/16en -hwWwu+i9NxCBj8crWH+cJCE22MAoyFUMNDKvlrFfXiUbi0+G56Hnnwivsz3kxEOq -Pt0hZE5Ian7ExfN7B74HMtU+Lu3/hmfU2ImlIv9Tlb31INhXRgwJSrVVR/7fqQIN -DNoRq+70wpUlToP6omN5LRGmFhb3MSWuuQ8kDQIDAQABAoIBAQDFiBkBvQVzxegM -ColDQN597K5PpDyesxV2BnBHTzXzvMZ8BPN1sWYm4jmOl2bH2y96sJo0y/y61Xrv -U+qqzk61nHA1k/JMyTEeBaWqCzay3JywGe51jwcotmgl9aT6n4ZwkYUZz23dEFVi -2FtHskKgvRCKJ7gn19FSvsJ68P/Dyl7H3/XGucj/7S+0JK3tb7BJ/ce68XABF99x -hvvkaWtxv0WNX2LWDyLVwv3T5i+pq4sscd9dmxwwCb1N3Lm3SkAOqH7BINia/qud -BLLJwHamzToWH7NTSWqrM4X9I7mI3zcMfOGeH9yZEFhB3cVu63V4yHfnGGqEiUOk -21fA+iLBAoGBAPXwZskl+nM0Z7yadaOOCqjRMdvPIgHOvQvjKtQJ/E7I4sH3ZBfO -4YPU0pErV4rbOyv6TZcUQwmcHmepK5wcHjj52+vgDQMr+K1wjRai8WdapKgXi39n -5IgPD0y5Hgi7qUJI6w67ybkawgknL8hm6TwtxfbKtVoJ5BVgS1UmFMYRAoGBANCN -e3X685aGqsyuCVU3bXnZVGyromiCDQge3NGuUFqaSCA0uK9/Q4HuStktH7LiRoZo -UwBmdnF0Wa4hMcjBBONv1bc8S43CdoJC3LR6DdFL8j4YarUSXnTFRo+MnKIbNwQh -378E1ws+dsOGrJ+IIqQJHfzsnG+vvb9PUleXgtI9AoGAFOBKKUri/oJ1R8oosDBv -cTMIs2rarSKaY3bt/L+4PgvJS8OvKGI0PFeFZDM0pCHF3Q7LJUbgBeHNpujyPbcZ -TabP5y7Gi/1gh4BlSYWdTjOghHAzNCZifLYii1WvWfhr/qdn5IFGN0MxM0uzP6SU -qboM8sz0JedvB+17l4e6/bECgYBYI0MHJGyns/ghEngtRISG13tfhdXYVwYM5YYr -M4EQGV3cBov610z/b2bAi9p2rjxh91sEs0jhP+vatHqmvjRDrnLiwp+npISTHpDJ -0T9fsboJ1iXaqo2yyeC9MA7OT7QbkflOcEw1m0tz7MmtjkodiyDaUGD4rowBexNw -oz6NfQKBgQCbhTO6MNmdeQrJn/ojR6HipypKqpVXqqqraAgU5BapaH0ZZwXkXDAM -36ldQviX8UnPNFqHj7jzVSNyWsgmKHnXFmdTEBYTd+0b+WEyn9FR/8kBlxHFR7Nc -AcAF7XF79pkJM31e6GCwFymYPbFJEL4TkWSOnPkypGY6IXHp57bKzA== ------END RSA PRIVATE KEY----- From 04e6177d5f976906c4e0bc6e76983c0db0871056 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 15 Mar 2017 00:03:27 +0000 Subject: [PATCH 056/136] more middleware added --- lib/local-web-server.js | 6 ++++-- package.json | 11 ++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index c477536..3b20b96 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -10,9 +10,11 @@ const Lws = require('lws') */ class LocalWebServer extends Lws { constructor () { - super({ - stack: [ 'log', 'static' ] + const path = require('path') + const stack = [ 'log', 'cors', 'json', 'rewrite', 'body-parser', 'blacklist', 'conditional-get', 'mime', 'compress', 'static', 'index' ].map(name => { + return path.resolve(__dirname, `../node_modules/local-web-server-${name}`) }) + super({ stack }) } } diff --git a/package.json b/package.json index 23e2efc..e58b02c 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,18 @@ "repository": "https://github.com/75lb/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { + "local-web-server-blacklist": "file:///Users/lloydb/Documents/lws/blacklist", + "local-web-server-body-parser": "file:///Users/lloydb/Documents/lws/body-parser", + "local-web-server-compress": "file:///Users/lloydb/Documents/lws/compress", + "local-web-server-conditional-get": "file:///Users/lloydb/Documents/lws/conditional-get", + "local-web-server-cors": "file:///Users/lloydb/Documents/lws/cors", + "local-web-server-index": "file:///Users/lloydb/Documents/lws/index", + "local-web-server-json": "file:///Users/lloydb/Documents/lws/json", "local-web-server-log": "file:///Users/lloydb/Documents/lws/log", + "local-web-server-mime": "file:///Users/lloydb/Documents/lws/mime", + "local-web-server-rewrite": "file:///Users/lloydb/Documents/lws/rewrite", "local-web-server-static": "file:///Users/lloydb/Documents/lws/static", - "lws": "^0.8.1" + "lws": "file:///Users/lloydb/Documents/lws/lws" }, "devDependencies": {} } From af2cf77a18da8b5dc6125cd5d3f935059eb545fc Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 15 Mar 2017 21:10:36 +0000 Subject: [PATCH 057/136] more middleware --- lib/local-web-server.js | 2 +- package.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 3b20b96..1fe7d30 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -11,7 +11,7 @@ const Lws = require('lws') class LocalWebServer extends Lws { constructor () { const path = require('path') - const stack = [ 'log', 'cors', 'json', 'rewrite', 'body-parser', 'blacklist', 'conditional-get', 'mime', 'compress', 'static', 'index' ].map(name => { + const stack = [ 'log', 'cors', 'json', 'rewrite', 'body-parser', 'blacklist', 'conditional-get', 'mime', 'compress', 'mock-response', 'spa', 'static', 'index' ].map(name => { return path.resolve(__dirname, `../node_modules/local-web-server-${name}`) }) super({ stack }) diff --git a/package.json b/package.json index e58b02c..0613843 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,9 @@ "local-web-server-json": "file:///Users/lloydb/Documents/lws/json", "local-web-server-log": "file:///Users/lloydb/Documents/lws/log", "local-web-server-mime": "file:///Users/lloydb/Documents/lws/mime", + "local-web-server-mock-response": "file:///Users/lloydb/Documents/lws/mock-response", "local-web-server-rewrite": "file:///Users/lloydb/Documents/lws/rewrite", + "local-web-server-spa": "file:///Users/lloydb/Documents/lws/spa", "local-web-server-static": "file:///Users/lloydb/Documents/lws/static", "lws": "file:///Users/lloydb/Documents/lws/lws" }, From d0797a703fc3d5a8dee6771065d4eac5be4afa8e Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 16 Mar 2017 23:10:06 +0000 Subject: [PATCH 058/136] version.. paths --- lib/cli-view.js | 56 ------------------------------------------------- lib/local-web-server.js | 10 +++++++-- 2 files changed, 8 insertions(+), 58 deletions(-) delete mode 100644 lib/cli-view.js diff --git a/lib/cli-view.js b/lib/cli-view.js deleted file mode 100644 index eaba5a4..0000000 --- a/lib/cli-view.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict' - -class CliView { - constructor (localWebServer) { - this.localWebServer = localWebServer - } - /** - * @example - * { log: 'whatever' } - * { config: { static: { root: 1, hidden: 2 } } } - */ - write (msg) { - const writeToStdout = [ 'log', 'info' ] - Object.keys(msg).forEach(key => { - if (writeToStdout.includes(key)) { - console.log(msg[key]) - } else if (key === 'config' && msg.config && this.localWebServer.options.verbose) { - printLine(msg.config) - } else if (key === 'error') { - const ansi = require('ansi-escape-sequences') - console.error(ansi.format(msg.error, 'red')) - } - }) - } -} - -module.exports = CliView - -function printLine (config) { - const output = objectToTable(config) - process.stderr.write(output) -} - -/** - * create a nested table for deep object trees - */ -function objectToTable (object) { - const ansi = require('ansi-escape-sequences') - const tableLayout = require('table-layout') - const t = require('typical') - - const data = Object.keys(object).map(key => { - if (t.isObject(object[key])) { - return { key: ansi.format(key, 'bold'), value: objectToTable(object[key]) } - } else { - return { key: ansi.format(key, 'bold'), value: object[key] } - } - }) - return tableLayout(data, { - padding: { left: '', right: ' ' }, - columns: [ - // { name: 'key', width: 18 }, - // { name: 'value', nowrap: true } - ] - }) -} diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 1fe7d30..c8fe200 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -11,11 +11,17 @@ const Lws = require('lws') class LocalWebServer extends Lws { constructor () { const path = require('path') - const stack = [ 'log', 'cors', 'json', 'rewrite', 'body-parser', 'blacklist', 'conditional-get', 'mime', 'compress', 'mock-response', 'spa', 'static', 'index' ].map(name => { - return path.resolve(__dirname, `../node_modules/local-web-server-${name}`) + const stack = [ 'lws-log', 'local-web-server-cors', 'local-web-server-json', 'local-web-server-rewrite', 'local-web-server-body-parser', 'local-web-server-blacklist', 'local-web-server-conditional-get', 'local-web-server-mime', 'local-web-server-compress', 'local-web-server-mock-response', 'local-web-server-spa', 'local-web-server-static', 'local-web-server-index' ].map(name => { + return path.resolve(__dirname, `../node_modules/${name}`) }) super({ stack }) } + + getVersion () { + const path = require('path') + const pkg = require(path.resolve(__dirname, '..', 'package.json')) + return pkg.version + } } module.exports = LocalWebServer From 08ab65d460ace129e9e71a79991848dc9eb03f97 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 17 Mar 2017 21:53:50 +0000 Subject: [PATCH 059/136] log.. rewrite.. config-name --- bin/cli.js | 2 +- lib/local-web-server.js | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index f8c6095..7348645 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env node --harmony_string_padding 'use strict' const LocalWebServer = require('../') const localWebServer = new LocalWebServer() diff --git a/lib/local-web-server.js b/lib/local-web-server.js index c8fe200..9e3e217 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -11,10 +11,10 @@ const Lws = require('lws') class LocalWebServer extends Lws { constructor () { const path = require('path') - const stack = [ 'lws-log', 'local-web-server-cors', 'local-web-server-json', 'local-web-server-rewrite', 'local-web-server-body-parser', 'local-web-server-blacklist', 'local-web-server-conditional-get', 'local-web-server-mime', 'local-web-server-compress', 'local-web-server-mock-response', 'local-web-server-spa', 'local-web-server-static', 'local-web-server-index' ].map(name => { + const stack = [ 'lws-log', 'local-web-server-cors', 'local-web-server-json', 'lws-rewrite', 'local-web-server-body-parser', 'local-web-server-blacklist', 'local-web-server-conditional-get', 'local-web-server-mime', 'local-web-server-compress', 'local-web-server-mock-response', 'local-web-server-spa', 'local-web-server-static', 'local-web-server-index' ].map(name => { return path.resolve(__dirname, `../node_modules/${name}`) }) - super({ stack }) + super({ stack, 'config-name': 'local-web-server' }) } getVersion () { diff --git a/package.json b/package.json index 0613843..4b7429e 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,10 @@ "local-web-server-cors": "file:///Users/lloydb/Documents/lws/cors", "local-web-server-index": "file:///Users/lloydb/Documents/lws/index", "local-web-server-json": "file:///Users/lloydb/Documents/lws/json", - "local-web-server-log": "file:///Users/lloydb/Documents/lws/log", + "lws-log": "file:///Users/lloydb/Documents/lws/log", "local-web-server-mime": "file:///Users/lloydb/Documents/lws/mime", "local-web-server-mock-response": "file:///Users/lloydb/Documents/lws/mock-response", - "local-web-server-rewrite": "file:///Users/lloydb/Documents/lws/rewrite", + "lws-rewrite": "file:///Users/lloydb/Documents/lws/rewrite", "local-web-server-spa": "file:///Users/lloydb/Documents/lws/spa", "local-web-server-static": "file:///Users/lloydb/Documents/lws/static", "lws": "file:///Users/lloydb/Documents/lws/lws" From cc08ca746515484b4464acf926b81d14ce345e7f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 19 Mar 2017 23:13:41 +0000 Subject: [PATCH 060/136] use lws packages --- example/built-in/rewrite/lws.config.js | 8 ++++++++ lib/local-web-server.js | 16 +++++++++++++++- package.json | 30 +++++++++++++++--------------- 3 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 example/built-in/rewrite/lws.config.js diff --git a/example/built-in/rewrite/lws.config.js b/example/built-in/rewrite/lws.config.js new file mode 100644 index 0000000..0d72234 --- /dev/null +++ b/example/built-in/rewrite/lws.config.js @@ -0,0 +1,8 @@ +module.exports = { + rewrite: [ + { from: '/css/*', 'to': '/build/styles/$1' }, + { from: '/npm/*', 'to': 'http://registry.npmjs.org/$1' }, + { from: '/broken/*', 'to': 'http://localhost:9999' }, + { from: '/:user/repos/:name', 'to': 'https://api.github.com/repos/:user/:name' } + ] +} diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 9e3e217..c17f2c4 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -11,7 +11,21 @@ const Lws = require('lws') class LocalWebServer extends Lws { constructor () { const path = require('path') - const stack = [ 'lws-log', 'local-web-server-cors', 'local-web-server-json', 'lws-rewrite', 'local-web-server-body-parser', 'local-web-server-blacklist', 'local-web-server-conditional-get', 'local-web-server-mime', 'local-web-server-compress', 'local-web-server-mock-response', 'local-web-server-spa', 'local-web-server-static', 'local-web-server-index' ].map(name => { + const stack = [ + 'lws-log', + 'lws-cors', + 'lws-json', + 'lws-rewrite', + 'lws-body-parser', + 'lws-blacklist', + 'lws-conditional-get', + 'lws-mime', + 'lws-compress', + 'lws-mock-response', + 'lws-spa', + 'lws-static', + 'lws-index' + ].map(name => { return path.resolve(__dirname, `../node_modules/${name}`) }) super({ stack, 'config-name': 'local-web-server' }) diff --git a/package.json b/package.json index 4b7429e..f529e8e 100644 --- a/package.json +++ b/package.json @@ -34,23 +34,23 @@ "docs": "jsdoc2md -t jsdoc2md/api.hbs -p list lib/*.js > doc/api.md; echo", "cover": "istanbul cover ./node_modules/.bin/tape test/*.js && cat coverage/lcov.info | coveralls && rm -rf coverage; echo" }, - "repository": "https://github.com/75lb/local-web-server", + "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "local-web-server-blacklist": "file:///Users/lloydb/Documents/lws/blacklist", - "local-web-server-body-parser": "file:///Users/lloydb/Documents/lws/body-parser", - "local-web-server-compress": "file:///Users/lloydb/Documents/lws/compress", - "local-web-server-conditional-get": "file:///Users/lloydb/Documents/lws/conditional-get", - "local-web-server-cors": "file:///Users/lloydb/Documents/lws/cors", - "local-web-server-index": "file:///Users/lloydb/Documents/lws/index", - "local-web-server-json": "file:///Users/lloydb/Documents/lws/json", - "lws-log": "file:///Users/lloydb/Documents/lws/log", - "local-web-server-mime": "file:///Users/lloydb/Documents/lws/mime", - "local-web-server-mock-response": "file:///Users/lloydb/Documents/lws/mock-response", - "lws-rewrite": "file:///Users/lloydb/Documents/lws/rewrite", - "local-web-server-spa": "file:///Users/lloydb/Documents/lws/spa", - "local-web-server-static": "file:///Users/lloydb/Documents/lws/static", - "lws": "file:///Users/lloydb/Documents/lws/lws" + "lws-blacklist": "^0.1.0", + "lws-body-parser": "^0.1.0", + "lws-compress": "^0.1.0", + "lws-conditional-get": "^0.1.0", + "lws-cors": "^0.1.0", + "lws-index": "^0.1.0", + "lws-json": "^0.1.0", + "lws-log": "^0.1.0", + "lws-mime": "^0.1.0", + "lws-mock-response": "^0.1.0", + "lws-rewrite": "^0.1.0", + "lws-spa": "^0.1.0", + "lws-static": "^0.1.0", + "lws": "^1.0.0-pre.0" }, "devDependencies": {} } From 24e8a2c0bb94dc9976b84fd1bdae1c34304b1f07 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Mar 2017 23:32:24 +0000 Subject: [PATCH 061/136] remove padEnd --- bin/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/cli.js b/bin/cli.js index 7348645..f8c6095 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node --harmony_string_padding +#!/usr/bin/env node 'use strict' const LocalWebServer = require('../') const localWebServer = new LocalWebServer() From ab2d2502593422b3a4a430bffc1333ce5e27496d Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Mar 2017 23:36:44 +0000 Subject: [PATCH 062/136] upgrade deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f529e8e..88ff902 100644 --- a/package.json +++ b/package.json @@ -44,13 +44,13 @@ "lws-cors": "^0.1.0", "lws-index": "^0.1.0", "lws-json": "^0.1.0", - "lws-log": "^0.1.0", + "lws-log": "^0.1.1", "lws-mime": "^0.1.0", "lws-mock-response": "^0.1.0", "lws-rewrite": "^0.1.0", "lws-spa": "^0.1.0", "lws-static": "^0.1.0", - "lws": "^1.0.0-pre.0" + "lws": "^1.0.0-pre.2" }, "devDependencies": {} } From 35e72769183d2128b8bad09e4e8c24555409311a Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 20 Mar 2017 23:36:52 +0000 Subject: [PATCH 063/136] 2.0.0-pre.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88ff902..ebf393d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-2", + "version": "2.0.0-pre.0", "description": "A simple web-server for productive front-end development", "bin": { "ws": "./bin/cli.js" From 6b7f7a953629e775541bef4eaf1c700f0c69c077 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 21 Mar 2017 23:46:53 +0000 Subject: [PATCH 064/136] basic test --- lib/local-web-server.js | 5 +++-- package.json | 6 ++++-- test/common.js | 22 --------------------- test/fixture/one.txt | 1 + test/test-middleware.js | 12 ------------ test/test.js | 52 +++++++++++++------------------------------------ 6 files changed, 21 insertions(+), 77 deletions(-) delete mode 100644 test/common.js create mode 100644 test/fixture/one.txt delete mode 100644 test/test-middleware.js diff --git a/lib/local-web-server.js b/lib/local-web-server.js index c17f2c4..8eba429 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -9,7 +9,7 @@ const Lws = require('lws') * @alias module:local-web-server */ class LocalWebServer extends Lws { - constructor () { + constructor (options) { const path = require('path') const stack = [ 'lws-log', @@ -28,7 +28,8 @@ class LocalWebServer extends Lws { ].map(name => { return path.resolve(__dirname, `../node_modules/${name}`) }) - super({ stack, 'config-name': 'local-web-server' }) + options = Object.assign({ stack }, options) + super(options) } getVersion () { diff --git a/package.json b/package.json index ebf393d..80b0e25 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "proxy" ], "engines": { - "node": ">=4.0.0" + "node": ">=7.6" }, "files": [ "bin", @@ -52,5 +52,7 @@ "lws-static": "^0.1.0", "lws": "^1.0.0-pre.2" }, - "devDependencies": {} + "devDependencies": { + "test-runner": "^0.3.0" + } } diff --git a/test/common.js b/test/common.js deleted file mode 100644 index 43d4ff1..0000000 --- a/test/common.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' -const arrayify = require('array-back') - -exports.checkResponse = checkResponse -exports.fail = fail - -function checkResponse (t, status, bodyTests) { - return function (response) { - if (status) t.strictEqual(response.res.statusCode, status) - if (bodyTests) { - arrayify(bodyTests).forEach(body => { - t.ok(body.test(response.data), 'correct data') - }) - } - } -} - -function fail (t) { - return function (err) { - t.fail('failed: ' + err.stack) - } -} diff --git a/test/fixture/one.txt b/test/fixture/one.txt new file mode 100644 index 0000000..5626abf --- /dev/null +++ b/test/fixture/one.txt @@ -0,0 +1 @@ +one diff --git a/test/test-middleware.js b/test/test-middleware.js deleted file mode 100644 index 137438c..0000000 --- a/test/test-middleware.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -class TestMiddleware { - middleware (option) { - return function (ctx, next) { - ctx.body = '1234512345' - return next() - } - } -} - -module.exports = TestMiddleware diff --git a/test/test.js b/test/test.js index b1ba7ea..e7054b6 100644 --- a/test/test.js +++ b/test/test.js @@ -1,46 +1,20 @@ 'use strict' -const test = require('tape') +const TestRunner = require('test-runner') const request = require('req-then') const LocalWebServer = require('../') -const c = require('./common') -const path = require('path') +const a = require('assert') -test('stack', function (t) { - t.plan(2) - const ws = new LocalWebServer({ - stack: [ path.resolve(__dirname, 'test-middleware.js') ], - port: 8100, - testMode: true - }) - ws.server.on('listening', () => { - return request('http://localhost:8100/') - .then(c.checkResponse(t, 200, /1234512345/)) - .then(ws.server.close.bind(ws.server)) - .catch(err => { - t.fail(err.message) - ws.server.close() - }) - }) -}) +const runner = new TestRunner() -test('https', function (t) { - t.plan(2) - const ws = new LocalWebServer({ - stack: [ path.resolve(__dirname, 'test-middleware.js') ], - https: true, - port: 8100, - testMode: true - }) - const url = require('url') - const reqOptions = url.parse('https://localhost:8100/') - reqOptions.rejectUnauthorized = false - ws.server.on('listening', () => { - return request(reqOptions) - .then(c.checkResponse(t, 200, /1234512345/)) - .then(ws.server.close.bind(ws.server)) - .catch(err => { - t.fail(err.message) - ws.server.close() - }) +runner.test('basic', async function () { + const port = 9000 + this.index + const localWebServer = new LocalWebServer({ + port: port, + 'static.root': 'test/fixture', + 'log.format': 'none' }) + localWebServer.start() + const response = await request(`http://localhost:${port}/one.txt`) + localWebServer.server.close() + a.strictEqual(response.data.toString(), 'one\n') }) From ec76624f669dec5f9400b7e0672eac411efe1660 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 23 Mar 2017 21:42:21 +0000 Subject: [PATCH 065/136] update repo URLs --- README.md | 12 ++++----- doc/blacklist.md | 2 +- doc/https.md | 2 +- doc/logging.md | 2 +- doc/mime-types.md | 2 +- doc/mock-response.md | 2 +- doc/rewrite.md | 2 +- doc/spa.md | 2 +- doc/visualisation.md | 2 +- example/README.md | 3 --- example/features/vanilla.js | 65 --------------------------------------------- lib/local-web-server.js | 13 +++++++++ package.json | 6 ++--- 13 files changed, 30 insertions(+), 85 deletions(-) delete mode 100644 example/README.md delete mode 100644 example/features/vanilla.js diff --git a/README.md b/README.md index cc529ac..3581f29 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ [![view on npm](http://img.shields.io/npm/v/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) [![npm module downloads](http://img.shields.io/npm/dt/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) -[![Build Status](https://travis-ci.org/75lb/local-web-server.svg?branch=master)](https://travis-ci.org/75lb/local-web-server) -[![Dependency Status](https://david-dm.org/75lb/local-web-server.svg)](https://david-dm.org/75lb/local-web-server) +[![Build Status](https://travis-ci.org/lwsjs/local-web-server.svg?branch=master)](https://travis-ci.org/lwsjs/local-web-server) +[![Dependency Status](https://david-dm.org/lwsjs/local-web-server.svg)](https://david-dm.org/lwsjs/local-web-server) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) -[![Join the chat at https://gitter.im/75lb/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/75lb/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join the chat at https://gitter.im/lwsjs/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lwsjs/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -***Requires node v4.0.0 or higher. Install the [previous release](https://github.com/75lb/local-web-server/tree/prev) for older node support.*** +***Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/prev) for older node support.*** # local-web-server At its core, local-web-server is an application shell for building a specialised command-line web server to support productive Web Platform engineers. When combined with built-in and custom features it's in intended to by a powerful tool in helping build and debug Web applications. It comes bundled with a middleware stack covering common requirements but any arbitrary stack can be specified from the command line or config. @@ -37,7 +37,7 @@ Being an npm module, it is trivial is bundle and distribute/deploy with your web * Rewrite routes to local or remote resources * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) - * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/75lb/local-web-server/blob/master/doc/visualisation.md) + * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/local-web-server/blob/master/doc/visualisation.md) * Proxy server * Map local routes to remote servers. Removes CORS pain when consuming remote services. * Back-end service mocking @@ -91,7 +91,7 @@ local-web-server is a command-line tool. To serve the current directory, run `ws -h, --help Print these usage instructions. --config Print the stored config. - Project home: https://github.com/75lb/local-web-server + Project home: https://github.com/lwsjs/local-web-server
    diff --git a/doc/blacklist.md b/doc/blacklist.md index 6793d50..23a4d52 100644 --- a/doc/blacklist.md +++ b/doc/blacklist.md @@ -6,4 +6,4 @@ $ ws --forbid '*.json' '*.yml' serving at http://localhost:8000 ``` -[Example](https://github.com/75lb/local-web-server/tree/master/example/forbid). +[Example](https://github.com/lwsjs/local-web-server/tree/master/example/forbid). diff --git a/doc/https.md b/doc/https.md index 8802915..5867f0a 100644 --- a/doc/https.md +++ b/doc/https.md @@ -56,4 +56,4 @@ Chrome and Firefox will still complain your certificate has not been verified by Now you have a valid, trusted certificate for development. ### Built-in certificate -As a quick win, you can run `ws` with the `https` flag. This will launch an HTTPS server using a [built-in certificate](https://github.com/75lb/local-web-server/tree/master/ssl) registered to the domain 127.0.0.1. +As a quick win, you can run `ws` with the `https` flag. This will launch an HTTPS server using a [built-in certificate](https://github.com/lwsjs/local-web-server/tree/master/ssl) registered to the domain 127.0.0.1. diff --git a/doc/logging.md b/doc/logging.md index 593c0ca..3883d7b 100644 --- a/doc/logging.md +++ b/doc/logging.md @@ -48,7 +48,7 @@ Then pipe the `logstalgia` output format directly into logstalgia for real-time $ ws -f logstalgia | logstalgia - ``` -![local-web-server with logstalgia](https://raw.githubusercontent.com/75lb/local-web-server/master/doc/img/logstagia.gif) +![local-web-server with logstalgia](https://raw.githubusercontent.com/lwsjs/local-web-server/master/doc/img/logstagia.gif) ## glTail To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format: diff --git a/doc/mime-types.md b/doc/mime-types.md index 3251230..03d78fe 100644 --- a/doc/mime-types.md +++ b/doc/mime-types.md @@ -9,4 +9,4 @@ You can set additional mime-type/extension mappings, or override the defaults by } ``` -[Example](https://github.com/75lb/local-web-server/tree/master/example/mime-override). +[Example](https://github.com/lwsjs/local-web-server/tree/master/example/mime-override). diff --git a/doc/mock-response.md b/doc/mock-response.md index 2d2b796..a6fdc4d 100644 --- a/doc/mock-response.md +++ b/doc/mock-response.md @@ -238,4 +238,4 @@ const mockResponses = [ module.exports = mockResponses ``` -[Example](https://github.com/75lb/local-web-server/tree/master/example/mock). \ No newline at end of file +[Example](https://github.com/lwsjs/local-web-server/tree/master/example/mock). \ No newline at end of file diff --git a/doc/rewrite.md b/doc/rewrite.md index af1907a..1120a92 100644 --- a/doc/rewrite.md +++ b/doc/rewrite.md @@ -34,4 +34,4 @@ Map local requests for repo data to the Github API: $ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name' ``` -[Example](https://github.com/75lb/local-web-server/tree/master/example/rewrite). +[Example](https://github.com/lwsjs/local-web-server/tree/master/example/rewrite). diff --git a/doc/spa.md b/doc/spa.md index 1c277e6..1a05b6e 100644 --- a/doc/spa.md +++ b/doc/spa.md @@ -9,4 +9,4 @@ By default, typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not F *If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* -[Example](https://github.com/75lb/local-web-server/tree/master/example/spa). +[Example](https://github.com/lwsjs/local-web-server/tree/master/example/spa). diff --git a/doc/visualisation.md b/doc/visualisation.md index c31ce9d..2ca61d9 100644 --- a/doc/visualisation.md +++ b/doc/visualisation.md @@ -35,7 +35,7 @@ Then pipe the `logstalgia` output format directly into logstalgia for real-time $ ws -f logstalgia | logstalgia - ``` -![local-web-server with logstalgia](https://raw.githubusercontent.com/75lb/local-web-server/master/doc/img/logstagia.gif) +![local-web-server with logstalgia](https://raw.githubusercontent.com/lwsjs/local-web-server/master/doc/img/logstagia.gif) ## glTail To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format: diff --git a/example/README.md b/example/README.md deleted file mode 100644 index 0e12d08..0000000 --- a/example/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Examples - -Some examples of how to use the built-in middleware and configure custom servers. diff --git a/example/features/vanilla.js b/example/features/vanilla.js deleted file mode 100644 index 1617454..0000000 --- a/example/features/vanilla.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict' - -class Yeah { - middleware () { - return function (req, res, next) { - res.end('Yeah?') - next() - } - } -} - -class Logger { - middleware () { - const express = require('express') - const app = express() - app.use((req, res, next) => { - console.log('incoming', req.url) - next() - }) - return app - } -} - -class Header { - middleware () { - return function (req, res, next) { - res.setHeader('x-pointless', 'yeah?') - next() - } - } -} - -class PieHeader { - middleware () { - const Koa = require('koa') - const app = new Koa() - app.use((ctx, next) => { - ctx.set('x-pie', 'steak and kidney') - next() - }) - return app.callback() - } -} - -const http = require('http') -const server = http.createServer() -server.listen(8100) -const yeah = new Yeah() -const logger = new Logger() -const header = new Header() -const pie = new PieHeader() -const stack = [ - logger.middleware(), - header.middleware(), - pie.middleware(), - yeah.middleware() -] -server.on('request', function (req, res) { - let index = 0 - function processNext () { - const mw = stack[index++] - if (mw) mw(req, res, processNext) - } - processNext() -}) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 8eba429..fc889a2 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -37,6 +37,19 @@ class LocalWebServer extends Lws { const pkg = require(path.resolve(__dirname, '..', 'package.json')) return pkg.version } + + getUsageHeader () { + return { + header: 'local-web-server', + content: 'A convenient local web server to support productive, full-stack Javascript development.' + } + } + + getUsageFooter () { + return { + content: 'Project home: [underline]{https://github.com/lwsjs/local-web-server}' + } + } } module.exports = LocalWebServer diff --git a/package.json b/package.json index 80b0e25..42ed72e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "local-web-server", "version": "2.0.0-pre.0", - "description": "A simple web-server for productive front-end development", + "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" }, @@ -49,8 +49,8 @@ "lws-mock-response": "^0.1.0", "lws-rewrite": "^0.1.0", "lws-spa": "^0.1.0", - "lws-static": "^0.1.0", - "lws": "^1.0.0-pre.2" + "lws-static": "^0.1.1", + "lws": "^1.0.0-pre.3" }, "devDependencies": { "test-runner": "^0.3.0" From d42f453ef30fc443cab150f800f495261c543ae7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 23 Mar 2017 23:17:14 +0000 Subject: [PATCH 066/136] readme.. fix test --- README.md | 114 +++++++++++++++++++++++++++++++---------------------------- package.json | 4 +-- test/test.js | 2 +- 3 files changed, 63 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 3581f29..04b9800 100644 --- a/README.md +++ b/README.md @@ -5,96 +5,102 @@ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) [![Join the chat at https://gitter.im/lwsjs/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lwsjs/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -***Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/prev) for older node support.*** +***Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/v1.x) for older node support. Documentation still WIP.*** # local-web-server -At its core, local-web-server is an application shell for building a specialised command-line web server to support productive Web Platform engineers. When combined with built-in and custom features it's in intended to by a powerful tool in helping build and debug Web applications. It comes bundled with a middleware stack covering common requirements but any arbitrary stack can be specified from the command line or config. -Being an npm module, it is trivial is bundle and distribute/deploy with your web application. +A convenient local web server to support productive, full-stack Javascript development. Built on [lws](https://github.com/lwsjs/lws). -**Typically used for building:** +**Features** -* Simple static site -* Single Page Application - * Works well with React, Angular or vanilla JS. +- Lightweight +- http/https ([http2](https://github.com/nodejs/http2) will be added once ready) +- Rewrite routes to local or remote resources + - Url rewriting + - Proxy certain routes to a remote server (e.g. an existing API). Avoids CORS pain when consuming remote services. +- Configurable by command-line options, stored config or both +- Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) +- Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/local-web-server/blob/master/doc/visualisation.md) +- Configurable CORS rules. All origins allowed by default. -**Backend scenarios covered:** +**Use cases** -* Existing API -* Mock API -* Websocket server - -**Server options** - -* HTTP or HTTPS server - * HTTPS is strictly required by some modern techs (ServiceWorker, Media Capture and Streams etc.) -* Configurable middleware stack - * Use any combination of built-in and custom middleware - * specify options (for command line or config) - * Accepts Koa v1 or 2 middleware - -**Built-in Middleware stack** - - * Rewrite routes to local or remote resources - * Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) - * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/local-web-server/blob/master/doc/visualisation.md) - * Proxy server - * Map local routes to remote servers. Removes CORS pain when consuming remote services. - * Back-end service mocking - * Prototype a web service, microservice, REST API etc. - * Mocks are defined with config (static), or code (dynamic). - * CORS-friendly, all origins allowed by default. - -**Personalised stack** +Things you can build: +- Simple static website +- Single Page Application + - Works well with React, Angular or vanilla JS. +- Real or mock web services + - e.g. a RESTful API or microservice + - Mocks are defined with config (static), or code (dynamic). +- Websocket server ## Synopsis + local-web-server is a command-line tool. To serve the current directory, run `ws`.
    $ ws --help
     
     local-web-server
     
    -  A simple web-server for productive front-end development.
    +  A convenient local web server to support productive, full-stack Javascript
    +  development.
     
     Synopsis
     
    -  $ ws [--verbose] [] []
    +  $ ws [--verbose] [--config-file file] [] []
       $ ws --config
       $ ws --help
    +  $ ws --version
    +
    +General
    +
    +  -h, --help               Print these usage instructions.
    +  --config                 Print the active config.
    +  -c, --config-file file   Config filename to use, defaults to "lws.config.js".
    +  -v, --verbose            Verbose output.
    +  --version                Print the version number.
     
     Server
     
    -  -p, --port number   Web server port.
    -  --key file          SSL key. Supply along with --cert to launch a https server.
    -  --cert file         SSL cert. Supply along with --key to launch a https server.
    -  --https             Enable HTTPS using a built-in key and cert, registered to the domain
    -                      127.0.0.1.
    +  -p, --port number     Web server port.
    +  --hostname string     The hostname (or IP address) to listen on. Defaults to 0.0.0.0.
    +  --stack feature ...   Feature stack.
    +  --key file            SSL key. Supply along with --cert to launch a https server.
    +  --cert file           SSL cert. Supply along with --key to launch a https server.
    +  --https               Enable HTTPS using a built-in key and cert, registered to the domain
    +                        127.0.0.1.
     
     Middleware
     
    +  -f, --log.format string        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', 'stats', or 'logstalgia') or supply a custom format (e.g.
    +                                 ':method -> :url').
    +  --cors.origin                  Access-Control-Allow-Origin value. Default is request Origin header.
    +  --cors.allow-methods           Access-Control-Allow-Methods value. Default is
    +                                 "GET,HEAD,PUT,POST,DELETE,PATCH"
       -r, --rewrite expression ...   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'.
       -b, --forbid path ...          A list of forbidden routes.
    -  -n, --no-cache                 Disable etag-based caching -forces loading from disk each request.
    -  -c, --compress                 Serve gzip-compressed resources, where applicable.
    -  -f, --log.format string        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').
    -  -s, --spa file                 Path to a Single Page App, e.g. app.html.
    +  -n, --no-cache                 Disable etag-based caching - forces loading from disk each request.
    +  -z, --compress                 Serve gzip-compressed resources, where applicable.
    +  --compress.threshold number    Minimum response size in bytes to apply compression. Defaults to 1024 bytes.
    +  --spa file                     Path to a Single Page App, e.g. app.html.
    +  --spa.asset-test RegExp        A regular expression to identify an asset file. Defaults to "\.".
       -d, --directory path           Root directory, defaults to the current directory.
    -
    -Misc
    -
    -  -h, --help    Print these usage instructions.
    -  --config      Print the stored config.
    +  --static.maxage number         Browser cache max-age in milliseconds.
    +  --static.defer                 If true, serves after `yield next`, allowing any downstream middleware to
    +                                 respond first.
    +  --static.index path            Default file name, defaults to `index.html`.
    +  --index.root path              Index root directory, defaults to --directory or the current directory.
    +  --index.hidden                 Show hidden files.
    +  --index.view name              Display mode, either `tiles` or `details`. Defaults to tiles.
     
       Project home: https://github.com/lwsjs/local-web-server
     
    - ## Install ```sh diff --git a/package.json b/package.json index 42ed72e..79a793e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "lws-compress": "^0.1.0", "lws-conditional-get": "^0.1.0", "lws-cors": "^0.1.0", - "lws-index": "^0.1.0", + "lws-index": "^0.1.1", "lws-json": "^0.1.0", "lws-log": "^0.1.1", "lws-mime": "^0.1.0", @@ -50,7 +50,7 @@ "lws-rewrite": "^0.1.0", "lws-spa": "^0.1.0", "lws-static": "^0.1.1", - "lws": "^1.0.0-pre.3" + "lws": "^1.0.0-pre.4" }, "devDependencies": { "test-runner": "^0.3.0" diff --git a/test/test.js b/test/test.js index e7054b6..394c7d6 100644 --- a/test/test.js +++ b/test/test.js @@ -10,7 +10,7 @@ runner.test('basic', async function () { const port = 9000 + this.index const localWebServer = new LocalWebServer({ port: port, - 'static.root': 'test/fixture', + directory: 'test/fixture', 'log.format': 'none' }) localWebServer.start() From 0bfa7a8434028dcace2d3230cc2b5b8e613d0139 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 23 Mar 2017 23:26:15 +0000 Subject: [PATCH 067/136] readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 04b9800..5ae1352 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![view on npm](http://img.shields.io/npm/v/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) [![npm module downloads](http://img.shields.io/npm/dt/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) -[![Build Status](https://travis-ci.org/lwsjs/local-web-server.svg?branch=master)](https://travis-ci.org/lwsjs/local-web-server) -[![Dependency Status](https://david-dm.org/lwsjs/local-web-server.svg)](https://david-dm.org/lwsjs/local-web-server) +[![Build Status](https://travis-ci.org/lwsjs/local-web-server.svg?branch=next)](https://travis-ci.org/lwsjs/local-web-server) +[![Dependency Status](https://david-dm.org/lwsjs/local-web-server/next.svg)](https://david-dm.org/lwsjs/local-web-server/next) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) [![Join the chat at https://gitter.im/lwsjs/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lwsjs/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -48,7 +48,7 @@ local-web-server is a command-line tool. To serve the current directory, run `ws Synopsis - $ ws [--verbose] [--config-file file] [] [] + $ ws [--verbose] [--config-file file] [<server options>] [<middleware options>] $ ws --config $ ws --help $ ws --version From fa582ffeba27e4bfac09f5375b89870fbb7d3f14 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 23 Mar 2017 23:26:46 +0000 Subject: [PATCH 068/136] 2.0.0-pre.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79a793e..bea0f6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre.0", + "version": "2.0.0-pre.1", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From 3a49b48c8900a779aafafc4ab48ed707006f17cd Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 27 Mar 2017 10:11:00 +0100 Subject: [PATCH 069/136] readme --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5ae1352..3f870d1 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,20 @@ # local-web-server -A convenient local web server to support productive, full-stack Javascript development. Built on [lws](https://github.com/lwsjs/lws). +The productive development web server. Built on [lws](https://github.com/lwsjs/lws). **Features** -- Lightweight -- http/https ([http2](https://github.com/nodejs/http2) will be added once ready) -- Rewrite routes to local or remote resources - - Url rewriting - - Proxy certain routes to a remote server (e.g. an existing API). Avoids CORS pain when consuming remote services. -- Configurable by command-line options, stored config or both -- Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) +- Fast and lightweight. Supports most common web application styles: + - Static site. + - Single Page Application (client-side rendering, web service consumption). + - Server-rendered content. +- Configurable with sensible defaults. Configure by constructor option, command-line option, stored config or all three. +- HTTP or HTTPS ([HTTP2](https://github.com/nodejs/http2) will be added once ready) +- URL rewriting + - Local rewrites for quick experimentation (e.g. from `/img/logo.svg` to `/img/new-logo.svg`) + - Rewrite to remote resources (e.g. from `/api/*` to `https://example-api.pl/api/$1`). *Note: ignores remote server's CORS policy, which during development is typically what you want*. +- Optimised default caching strategy. Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) - Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/local-web-server/blob/master/doc/visualisation.md) - Configurable CORS rules. All origins allowed by default. From 07294f3cbc282ff022a13633ccbb14f81489a0e1 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 14 May 2017 23:06:20 +0100 Subject: [PATCH 070/136] use module-dir --- README.md | 18 +++++++++--------- bin/cli.js | 11 ++++++++++- lib/local-web-server.js | 9 ++++----- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3f870d1..b346dcb 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,28 @@ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) [![Join the chat at https://gitter.im/lwsjs/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lwsjs/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -***Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/v1.x) for older node support. Documentation still WIP.*** +***Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/v1.x) for node >= v4.0.0. Documentation still WIP.*** # local-web-server -The productive development web server. Built on [lws](https://github.com/lwsjs/lws). +The development web server for productive front-end and full-stack Javascript engineers. Built on [lws](https://github.com/lwsjs/lws). **Features** -- Fast and lightweight. Supports most common web application styles: - - Static site. - - Single Page Application (client-side rendering, web service consumption). - - Server-rendered content. -- Configurable with sensible defaults. Configure by constructor option, command-line option, stored config or all three. +- Fast and lightweight. +- Use the built-in features, a subset of the built-ins or your own feature stack +- Configurable with sensible defaults. + - Configure by constructor option, command-line option, stored config or all three. - HTTP or HTTPS ([HTTP2](https://github.com/nodejs/http2) will be added once ready) - URL rewriting - Local rewrites for quick experimentation (e.g. from `/img/logo.svg` to `/img/new-logo.svg`) - Rewrite to remote resources (e.g. from `/api/*` to `https://example-api.pl/api/$1`). *Note: ignores remote server's CORS policy, which during development is typically what you want*. -- Optimised default caching strategy. Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) +- Optimisal caching by default. + - Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) - Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/local-web-server/blob/master/doc/visualisation.md) - Configurable CORS rules. All origins allowed by default. -**Use cases** +**Links to demoes and how-tos** Things you can build: diff --git a/bin/cli.js b/bin/cli.js index f8c6095..fce734c 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -2,4 +2,13 @@ 'use strict' const LocalWebServer = require('../') const localWebServer = new LocalWebServer() -localWebServer.start() +try { + localWebServer.start() +} catch (err) { + if (err.code === 'MODULE_NOT_FOUND') { + console.error(err.message) + console.error(require('util').inspect(err.attempted, { depth: 6, colors: true })) + } else { + console.error(err.stack) + } +} diff --git a/lib/local-web-server.js b/lib/local-web-server.js index fc889a2..fc71617 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -11,7 +11,7 @@ const Lws = require('lws') class LocalWebServer extends Lws { constructor (options) { const path = require('path') - const stack = [ + let stack = [ 'lws-log', 'lws-cors', 'lws-json', @@ -25,10 +25,9 @@ class LocalWebServer extends Lws { 'lws-spa', 'lws-static', 'lws-index' - ].map(name => { - return path.resolve(__dirname, `../node_modules/${name}`) - }) - options = Object.assign({ stack }, options) + ] + const moduleDir = path.resolve(__dirname, `../node_modules`) + options = Object.assign({ stack, 'module-dir': moduleDir, prefix: 'lws-' }, options) super(options) } From 1ef1813ea99c00f8c53c3c2d2f8d154080563229 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 23 May 2017 11:35:31 +0100 Subject: [PATCH 071/136] ws feature-list --- lib/feature-list.js | 22 ++++++++++++++++++++++ lib/local-web-server.js | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 lib/feature-list.js diff --git a/lib/feature-list.js b/lib/feature-list.js new file mode 100644 index 0000000..3326c8a --- /dev/null +++ b/lib/feature-list.js @@ -0,0 +1,22 @@ +class FeatureList { + execute (options) { + const list = [ + 'lws-log', + 'lws-cors', + 'lws-json', + 'lws-rewrite', + 'lws-body-parser', + 'lws-blacklist', + 'lws-conditional-get', + 'lws-mime', + 'lws-compress', + 'lws-mock-response', + 'lws-spa', + 'lws-static', + 'lws-index' + ] + console.log(list) + } +} + +module.exports = FeatureList diff --git a/lib/local-web-server.js b/lib/local-web-server.js index fc71617..721a41c 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -27,8 +27,11 @@ class LocalWebServer extends Lws { 'lws-index' ] const moduleDir = path.resolve(__dirname, `../node_modules`) - options = Object.assign({ stack, 'module-dir': moduleDir, prefix: 'lws-' }, options) + options = Object.assign({ stack, 'module-dir': moduleDir, 'module-prefix': 'lws-' }, options) super(options) + + /* add command */ + this.commands.set('feature-list', require('./feature-list')) } getVersion () { From 67468d865b2d3e95f7d44fbdd0c0f842213fabde Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 5 Jun 2017 23:41:17 +0100 Subject: [PATCH 072/136] override server stack --- lib/feature-list.js | 3 +++ lib/local-web-server.js | 65 ++++++++++++++++++++++++++++++++----------------- package.json | 2 +- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/lib/feature-list.js b/lib/feature-list.js index 3326c8a..27a4e31 100644 --- a/lib/feature-list.js +++ b/lib/feature-list.js @@ -1,4 +1,7 @@ class FeatureList { + description () { + return 'Print installed features' + } execute (options) { const list = [ 'lws-log', diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 721a41c..edab733 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,15 +1,13 @@ 'use strict' const Lws = require('lws') +const Serve = require('lws/lib/command/serve/serve') /** * @module local-web-server */ -/** - * @alias module:local-web-server - */ -class LocalWebServer extends Lws { - constructor (options) { +class WsServe extends Serve { + execute (options, argv) { const path = require('path') let stack = [ 'lws-log', @@ -27,11 +25,47 @@ class LocalWebServer extends Lws { 'lws-index' ] const moduleDir = path.resolve(__dirname, `../node_modules`) - options = Object.assign({ stack, 'module-dir': moduleDir, 'module-prefix': 'lws-' }, options) - super(options) + options = { + stack, + 'module-dir': moduleDir, + 'module-prefix': 'lws-' + } + super.execute(options, argv) + } - /* add command */ - this.commands.set('feature-list', require('./feature-list')) + usage () { + const sections = super.usage() + sections.shift() + sections.shift() + sections.pop() + sections.unshift( + { + header: 'local-web-server', + content: 'A convenient local web server to support productive, full-stack Javascript development.' + }, + { + header: 'Synopsis', + content: [ + '$ ws ', + '$ ws [underline]{command} ' + ] + } + ) + sections.push({ + content: 'Project home: [underline]{https://github.com/lwsjs/local-web-server}' + }) + return sections + } +} + +/** + * @alias module:local-web-server + */ +class LocalWebServer extends Lws { + constructor (options) { + super (options) + this.commands.add(null, WsServe) + this.commands.add('feature-list', require('./feature-list')) } getVersion () { @@ -39,19 +73,6 @@ class LocalWebServer extends Lws { const pkg = require(path.resolve(__dirname, '..', 'package.json')) return pkg.version } - - getUsageHeader () { - return { - header: 'local-web-server', - content: 'A convenient local web server to support productive, full-stack Javascript development.' - } - } - - getUsageFooter () { - return { - content: 'Project home: [underline]{https://github.com/lwsjs/local-web-server}' - } - } } module.exports = LocalWebServer diff --git a/package.json b/package.json index bea0f6e..78fcf7c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "lws-mime": "^0.1.0", "lws-mock-response": "^0.1.0", "lws-rewrite": "^0.1.0", - "lws-spa": "^0.1.0", + "lws-spa": "^0.1.2", "lws-static": "^0.1.1", "lws": "^1.0.0-pre.4" }, From 3f993f4ecd2bec92f22b500423e0a9245c1145a7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 7 Jun 2017 23:30:30 +0100 Subject: [PATCH 073/136] camel case options.. deps --- lib/local-web-server.js | 4 +- package-lock.json | 1352 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 14 +- 3 files changed, 1361 insertions(+), 9 deletions(-) create mode 100644 package-lock.json diff --git a/lib/local-web-server.js b/lib/local-web-server.js index edab733..1d5ad99 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -27,8 +27,8 @@ class WsServe extends Serve { const moduleDir = path.resolve(__dirname, `../node_modules`) options = { stack, - 'module-dir': moduleDir, - 'module-prefix': 'lws-' + moduleDir, + modulePrefix: 'lws-' } super.execute(options, argv) } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..10c22c5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1352 @@ +{ + "name": "local-web-server", + "version": "2.0.0-pre.1", + "lockfileVersion": 1, + "dependencies": { + "accepts": { + "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" + }, + "ansi-escape-sequences": { + "version": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz", + "integrity": "sha1-HBg5S2r5t2/5pjUJ+kl2af0s5T4=", + "dev": true + }, + "any-promise": { + "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "array-back": { + "version": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=" + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "batch": { + "version": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz", + "integrity": "sha1-PzQU84AyF0O/wQQvmoP/HVgk1GQ=" + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "dev": true + }, + "buffer-shims": { + "version": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "bytes": { + "version": "https://registry.npmjs.org/bytes/-/bytes-2.5.0.tgz", + "integrity": "sha1-TJQj6i0lLCcMQbK97+/5u2tiwGo=" + }, + "co": { + "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "https://registry.npmjs.org/co-body/-/co-body-5.1.1.tgz", + "integrity": "sha1-2XeB0eM0S6SoIP0YBr3fg0FQUjY=" + }, + "command-line-tool": { + "version": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.6.4.tgz", + "integrity": "sha1-TBHjcvPkElSGHD/mtTjTx6WxRPM=", + "dev": true, + "dependencies": { + "command-line-args": { + "version": "https://registry.npmjs.org/command-line-args/-/command-line-args-3.0.5.tgz", + "integrity": "sha1-W9StReeYPlwTRJGOQCgO4mk8WsA=", + "dev": true + }, + "command-line-usage": { + "version": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-3.0.8.tgz", + "integrity": "sha1-tqIJeMGzg0d/XBGlKUKLiAv+D00=", + "dev": true + }, + "table-layout": { + "version": "https://registry.npmjs.org/table-layout/-/table-layout-0.3.0.tgz", + "integrity": "sha1-buINxIPbNxs+XIf3BO0vfHmdLJo=", + "dev": true + } + } + }, + "compressible": { + "version": "https://registry.npmjs.org/compressible/-/compressible-2.0.10.tgz", + "integrity": "sha1-/tocf3YXkScyspv4zyYlKiC57s0=" + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "copy-to": { + "version": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, + "core-js": { + "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", + "dev": true + }, + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dependencies": { + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "deep-extend": { + "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "defer-promise": { + "version": "https://registry.npmjs.org/defer-promise/-/defer-promise-1.0.1.tgz", + "integrity": "sha1-HKb/7dvO8XFd16riXHYW+a4iky8=" + }, + "depd": { + "version": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + }, + "escape-html": { + "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" + }, + "feature-detect-es6": { + "version": "https://registry.npmjs.org/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz", + "integrity": "sha1-+IhzavnLDJH1VmO/pHYuuW7nBH8=", + "dev": true + }, + "file-set": { + "version": "https://registry.npmjs.org/file-set/-/file-set-1.1.1.tgz", + "integrity": "sha1-0+xwwIDsjxjyBLod4QZ4DJBWkms=", + "dev": true + }, + "find-replace": { + "version": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", + "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=", + "dev": true + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true + }, + "http-errors": { + "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", + "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=" + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", + "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" + }, + "inflation": { + "version": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "json-stringify-safe": { + "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "koa-bodyparser": { + "version": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.2.0.tgz", + "integrity": "sha1-vObgi8Zfhwm20fqpQRx/DYk4qlQ=" + }, + "koa-compress": { + "version": "https://registry.npmjs.org/koa-compress/-/koa-compress-2.0.0.tgz", + "integrity": "sha1-e36ykhuEd0a14SK6n1zYpnHo6jo=" + }, + "koa-conditional-get": { + "version": "https://registry.npmjs.org/koa-conditional-get/-/koa-conditional-get-2.0.0.tgz", + "integrity": "sha1-pD83I8HQFLcwo07Oit8wuTyCM/I=" + }, + "koa-etag": { + "version": "https://registry.npmjs.org/koa-etag/-/koa-etag-3.0.0.tgz", + "integrity": "sha1-nvc4Ld1agqsN6xU0FckVg293HT8=" + }, + "koa-is-json": { + "version": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "koa-json": { + "version": "https://registry.npmjs.org/koa-json/-/koa-json-2.0.2.tgz", + "integrity": "sha1-Nq8U5uofXWRtfESihXAcb4Wk/eQ=" + }, + "koa-mock-response": { + "version": "https://registry.npmjs.org/koa-mock-response/-/koa-mock-response-0.0.2.tgz", + "integrity": "sha1-/CnpoHvQva61p5oQB/NxQjRgsQ0=" + }, + "koa-rewrite": { + "version": "https://registry.npmjs.org/koa-rewrite/-/koa-rewrite-2.1.0.tgz", + "integrity": "sha1-gSs19IqGADG8hi+IE15cKDGLeqg=", + "dependencies": { + "path-to-regexp": { + "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.0.2.tgz", + "integrity": "sha1-SJ/rBgsxREOlSUqx2i7+0gQKskw=" + } + } + }, + "koa-route": { + "version": "https://registry.npmjs.org/koa-route/-/koa-route-3.2.0.tgz", + "integrity": "sha1-dimLmaa8+p44yrb+XHmocz51i84=" + }, + "koa-send": { + "version": "https://registry.npmjs.org/koa-send/-/koa-send-3.3.0.tgz", + "integrity": "sha1-WkriRVZGgMbs9geeknX6UXOoYdw=" + }, + "koa-static": { + "version": "https://registry.npmjs.org/koa-static/-/koa-static-3.0.0.tgz", + "integrity": "sha1-QEQiM9LAs1wiXkUBmcEL+KU55BY=" + }, + "lodash.pick": { + "version": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "lws": { + "version": "1.0.0-pre.4", + "dependencies": { + "accepts": { + "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" + }, + "ansi-escape-sequences": { + "version": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz", + "integrity": "sha1-HBg5S2r5t2/5pjUJ+kl2af0s5T4=" + }, + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "any-promise": { + "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "argparse": { + "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=" + }, + "array-back": { + "version": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=" + }, + "array-base": { + "version": "https://registry.npmjs.org/array-base/-/array-base-1.0.1.tgz", + "integrity": "sha1-6hM7DBLCX+MzCraELJmSd3Ghuvo=" + }, + "asn1": { + "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "asynckit": { + "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + }, + "bcrypt-pbkdf": { + "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true + }, + "boom": { + "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=" + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=" + }, + "byte-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-3.0.0.tgz", + "integrity": "sha1-QG+eI2aqXav2NnLrKR17sJSV2nU=" + }, + "caseless": { + "version": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=" + }, + "cli-commands": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-commands/-/cli-commands-0.3.1.tgz", + "integrity": "sha512-UloGvUiUEelYX8LlTORQaZwm2lOMR0goS+Ga+RlkLpoCpGv5QnwGsrwBO8UF0ZULdvuWsXNJqiOnAPGh9LyTjw==" + }, + "co": { + "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "combined-stream": { + "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=" + }, + "command-line-args": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.6.tgz", + "integrity": "sha1-D/h6HdFZiQ3K6yoAWr2uceVQWfw=" + }, + "command-line-commands": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/command-line-commands/-/command-line-commands-2.0.0.tgz", + "integrity": "sha1-Gr2hFjPo03vLGjn3O42Tmi2F7zk=" + }, + "command-line-tool": { + "version": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.6.4.tgz", + "integrity": "sha1-TBHjcvPkElSGHD/mtTjTx6WxRPM=", + "dependencies": { + "command-line-args": { + "version": "https://registry.npmjs.org/command-line-args/-/command-line-args-3.0.5.tgz", + "integrity": "sha1-W9StReeYPlwTRJGOQCgO4mk8WsA=" + }, + "command-line-usage": { + "version": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-3.0.8.tgz", + "integrity": "sha1-tqIJeMGzg0d/XBGlKUKLiAv+D00=" + }, + "table-layout": { + "version": "https://registry.npmjs.org/table-layout/-/table-layout-0.3.0.tgz", + "integrity": "sha1-buINxIPbNxs+XIf3BO0vfHmdLJo=" + } + } + }, + "command-line-usage": { + "version": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-4.0.0.tgz", + "integrity": "sha1-gWsyeItY+f66RNHm2sYPyuspteo=" + }, + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=" + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "content-disposition": { + "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" + }, + "cookies": { + "version": "https://registry.npmjs.org/cookies/-/cookies-0.7.0.tgz", + "integrity": "sha1-C8lh2RDDUlSYD8fJ7/XaEgEbvwA=" + }, + "core-js": { + "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "coveralls": { + "version": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.1.tgz", + "integrity": "sha1-1wu5rMGDXsTwY/+drFQjwXsR8Xg=" + }, + "cryptiles": { + "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=" + }, + "dashdash": { + "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" + }, + "deep-equal": { + "version": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, + "defer-promise": { + "version": "https://registry.npmjs.org/defer-promise/-/defer-promise-1.0.1.tgz", + "integrity": "sha1-HKb/7dvO8XFd16riXHYW+a4iky8=" + }, + "delayed-stream": { + "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + }, + "destroy": { + "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", + "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" + }, + "ecc-jsbn": { + "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true + }, + "ee-first": { + "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "error-inject": { + "version": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, + "escape-html": { + "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + }, + "extend": { + "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + }, + "feature-detect-es6": { + "version": "https://registry.npmjs.org/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz", + "integrity": "sha1-+IhzavnLDJH1VmO/pHYuuW7nBH8=" + }, + "file-set": { + "version": "https://registry.npmjs.org/file-set/-/file-set-1.1.1.tgz", + "integrity": "sha1-0+xwwIDsjxjyBLod4QZ4DJBWkms=" + }, + "find-replace": { + "version": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", + "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=" + }, + "forever-agent": { + "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=" + }, + "fresh": { + "version": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "generate-function": { + "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "generate-object-property": { + "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=" + }, + "getpass": { + "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=" + }, + "graceful-readlink": { + "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "handle-thing": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", + "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=" + }, + "har-validator": { + "version": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=" + }, + "has-ansi": { + "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=" + }, + "hawk": { + "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=" + }, + "hoek": { + "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=" + }, + "http-assert": { + "version": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", + "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=" + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + }, + "http-errors": { + "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", + "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=", + "dependencies": { + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "http-signature": { + "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=" + }, + "http2": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/http2/-/http2-3.3.6.tgz", + "integrity": "sha1-ffBiJ+ArW1pYQd7qCCObMZjQS+w=" + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-generator-function": { + "version": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.6.tgz", + "integrity": "sha1-nnFlPNFf/zQcecQVFGChMdMen8Q=" + }, + "is-my-json-valid": { + "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=" + }, + "is-property": { + "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "is-typedarray": { + "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jodid25519": { + "version": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", + "optional": true + }, + "js-yaml": { + "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=" + }, + "jsbn": { + "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stringify-safe": { + "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonpointer": { + "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, + "jsprim": { + "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "keygrip": { + "version": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.1.tgz", + "integrity": "sha1-sC+kgW7vIajEs1yp5Skh/8iaMOk=" + }, + "koa": { + "version": "https://registry.npmjs.org/koa/-/koa-2.2.0.tgz", + "integrity": "sha1-sFWTMYeEnVQK2Ln3MbqqS+l8ZS0=" + }, + "koa-compose": { + "version": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=" + }, + "koa-convert": { + "version": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=" + }, + "koa-is-json": { + "version": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "lcov-parse": { + "version": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=" + }, + "load-module": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/load-module/-/load-module-0.2.0.tgz", + "integrity": "sha512-u7vn0QrCaFlCg3/ehUAyMpf4cuzonoU0Rj1fDT2Fk9xD6EvQF7eoVJlLqeshiUtwZAYqS47SC4HAFnhkeVI3ZQ==" + }, + "lodash.assignwith": { + "version": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", + "integrity": "sha1-EnqX8CrcQXUalU0ksN4X4QDgOOs=" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, + "lodash.padend": { + "version": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", + "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" + }, + "lodash.pick": { + "version": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "log-driver": { + "version": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", + "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=" + }, + "media-typer": { + "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mime-db": { + "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + }, + "mime-types": { + "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=" + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "oauth-sign": { + "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "obuf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz", + "integrity": "sha1-EEEktsYCxnlogaBCVB0220OlJk4=" + }, + "on-finished": { + "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + }, + "only": { + "version": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "parseurl": { + "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "pinkie": { + "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" + }, + "readable-stream": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.11.tgz", + "integrity": "sha512-h+8+r3MKEhkiVrwdKL8aWs1oc1VvBu33ueshOvS26RsZQ3Amhx/oO3TKe4lApSV9ueY6as8EAh7mtuFjdlhg9Q==", + "dependencies": { + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" + } + } + }, + "reduce-flatten": { + "version": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", + "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" + }, + "req-then": { + "version": "https://registry.npmjs.org/req-then/-/req-then-0.6.1.tgz", + "integrity": "sha1-6Fr2uTn/zEtPFFt+UcrnPo+AUsk=" + }, + "request": { + "version": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=" + }, + "safe-buffer": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.0.tgz", + "integrity": "sha512-aSLEDudu6OoRr/2rU609gRmnYboRLxgDG1z9o2Q0os7236FwvcqIOO8r8U5JUEwivZOhDaKlFO4SbPTJYyBEyQ==" + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" + }, + "setprototypeof": { + "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "sntp": { + "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=" + }, + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=" + }, + "spdy-transport": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.0.20.tgz", + "integrity": "sha1-c15yBUxIayNU/onnAiVgBKOazk0=" + }, + "sprintf-js": { + "version": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", + "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "statuses": { + "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=" + }, + "stringstream": { + "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "table-layout": { + "version": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.0.tgz", + "integrity": "sha1-xw/wRV2a3WO5H3wVp3kmKVwODn0=" + }, + "test-runner": { + "version": "https://registry.npmjs.org/test-runner/-/test-runner-0.3.0.tgz", + "integrity": "sha1-0cmKEdFaA1vabvI0KhBOcCUajVg=" + }, + "test-value": { + "version": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", + "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=" + }, + "tough-cookie": { + "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=" + }, + "tunnel-agent": { + "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" + }, + "tweetnacl": { + "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-is": { + "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" + }, + "typical": { + "version": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", + "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + }, + "vary": { + "version": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" + }, + "verror": { + "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=" + }, + "walk-back": { + "version": "https://registry.npmjs.org/walk-back/-/walk-back-3.0.0.tgz", + "integrity": "sha1-I1h4ejXakQMtrV6S+AsSNw2HlcU=" + }, + "wbuf": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.2.tgz", + "integrity": "sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=" + }, + "wordwrapjs": { + "version": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-2.0.0.tgz", + "integrity": "sha1-q1X2leYRjak4WP3XDAU9HF4BrCA=" + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } + }, + "lws-blacklist": { + "version": "https://registry.npmjs.org/lws-blacklist/-/lws-blacklist-0.1.0.tgz", + "integrity": "sha1-LmSXREwA+mRiSNtziYZ3Y9WrWjo=" + }, + "lws-body-parser": { + "version": "https://registry.npmjs.org/lws-body-parser/-/lws-body-parser-0.1.0.tgz", + "integrity": "sha1-Ge/CamCCT1C59h+8Cn6CfIunvGU=" + }, + "lws-compress": { + "version": "https://registry.npmjs.org/lws-compress/-/lws-compress-0.1.0.tgz", + "integrity": "sha1-wuaNffpTReb00ZEZU1xpaF6AkJQ=" + }, + "lws-conditional-get": { + "version": "https://registry.npmjs.org/lws-conditional-get/-/lws-conditional-get-0.1.0.tgz", + "integrity": "sha1-UCX2Vf7W4oXA7+gQPkPp51EySpI=" + }, + "lws-cors": { + "version": "0.2.0", + "dependencies": { + "kcors": { + "version": "https://registry.npmjs.org/kcors/-/kcors-2.2.1.tgz", + "integrity": "sha1-cWCpTy6uYzQ20s746t0M4jI4Z3k=" + } + } + }, + "lws-index": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/lws-index/-/lws-index-0.2.0.tgz", + "integrity": "sha512-8+ezEKIOiTbcn1VWa0v7X0AP1JpoTaywVSL1YOrf41FQl4P5ibzg64jZAPBurX7CL5qjZbU+o/SqyFvkVgur5g==" + }, + "lws-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/lws-json/-/lws-json-0.2.0.tgz", + "integrity": "sha512-VOaVZj3waaH2+bm+M5S3vneENYlnkxkyn+usj6RU5tuiSEzCodSnK3cCtq/2FD0/WZt82i7s0mhjG7m2/akEnA==" + }, + "lws-log": { + "version": "0.2.0", + "dependencies": { + "ansi-escape-sequences": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz", + "integrity": "sha1-HBg5S2r5t2/5pjUJ+kl2af0s5T4=" + }, + "array-back": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", + "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=" + }, + "basic-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", + "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=" + }, + "byte-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-3.0.0.tgz", + "integrity": "sha1-QG+eI2aqXav2NnLrKR17sJSV2nU=" + }, + "common-log-format": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/common-log-format/-/common-log-format-0.1.3.tgz", + "integrity": "sha1-YAna3E7EZzca8oqNvT5M4CsWZ4I=" + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "feature-detect-es6": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz", + "integrity": "sha1-+IhzavnLDJH1VmO/pHYuuW7nBH8=" + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, + "JSONStream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=" + }, + "koa-morgan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", + "integrity": "sha1-CAUuDODYOdPEMXi5CluzQkvvH5k=" + }, + "lodash.padend": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", + "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" + }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + }, + "morgan": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.8.2.tgz", + "integrity": "sha1-eErHc05KRTqcbm6GgKkyknXItoc=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, + "reduce-flatten": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", + "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" + }, + "stream-log-stats": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-log-stats/-/stream-log-stats-2.0.2.tgz", + "integrity": "sha512-b1LccxXhMlOQQrzSqapQHyZ3UI00QTAv+8VecFgsJz//sGB5LFl/+mkFeWBVVI2/E4DlCT4sGgvLExB/VTVFfA==" + }, + "stream-via": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-via/-/stream-via-1.0.4.tgz", + "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==" + }, + "table-layout": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.0.tgz", + "integrity": "sha1-xw/wRV2a3WO5H3wVp3kmKVwODn0=" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "typical": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", + "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" + }, + "wordwrapjs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-2.0.0.tgz", + "integrity": "sha1-q1X2leYRjak4WP3XDAU9HF4BrCA=" + } + } + }, + "lws-mime": { + "version": "https://registry.npmjs.org/lws-mime/-/lws-mime-0.1.0.tgz", + "integrity": "sha1-m2wpzSZkDWHw2oEPvfIiTij7GU8=" + }, + "lws-mock-response": { + "version": "https://registry.npmjs.org/lws-mock-response/-/lws-mock-response-0.1.0.tgz", + "integrity": "sha1-Kucq7xyekOxLq0d3D7gTRtfV4vU=" + }, + "lws-rewrite": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/lws-rewrite/-/lws-rewrite-0.2.0.tgz", + "integrity": "sha512-DVw/FxIyAZWXTxiPGHH6yql2HnbWCUFK3ZpgSOpZ8Yzw7dxBbThW8vob2oAq3rIo8JGak0OWMmeLd9epvnvNrA==" + }, + "lws-spa": { + "version": "https://registry.npmjs.org/lws-spa/-/lws-spa-0.1.2.tgz", + "integrity": "sha1-l/2p1eG1vOyhpFWBdWVRkLEV6P4=", + "dependencies": { + "koa-send": { + "version": "https://registry.npmjs.org/koa-send/-/koa-send-4.1.0.tgz", + "integrity": "sha1-B9Wk6qshJnn+mZFqrmsRCcCMI2E=" + } + } + }, + "lws-static": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/lws-static/-/lws-static-0.2.0.tgz", + "integrity": "sha512-s/tin0Z4Bkg5O2vY8orJUhtPZWDSe/gTfMpyai4EGy1xn2kJ2gM6iM/auT1wysdVIxe9lCTN7iqBJlkn400l6g==" + }, + "media-typer": { + "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "methods": { + "version": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime-db": { + "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.28.0.tgz", + "integrity": "sha1-/t00m+BtKGW3/FfYN8beTxfXrDw=" + }, + "mime-types": { + "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "dependencies": { + "mime-db": { + "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + } + } + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" + }, + "mz": { + "version": "https://registry.npmjs.org/mz/-/mz-2.6.0.tgz", + "integrity": "sha1-yLhSHZWN8KTydoAl22nHGe5O8c4=" + }, + "negotiator": { + "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "parseurl": { + "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-to-regexp": { + "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=" + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "raw-body": { + "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", + "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=", + "dependencies": { + "bytes": { + "version": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" + } + } + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", + "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", + "dependencies": { + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, + "reduce-flatten": { + "version": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", + "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=", + "dev": true + }, + "req-then": { + "version": "https://registry.npmjs.org/req-then/-/req-then-0.6.1.tgz", + "integrity": "sha1-6Fr2uTn/zEtPFFt+UcrnPo+AUsk=" + }, + "resolve-path": { + "version": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.3.3.tgz", + "integrity": "sha1-TYOrpkaMK45jKldeP1Kw+g2+Glw=", + "dependencies": { + "http-errors": { + "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", + "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=" + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "setprototypeof": { + "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", + "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=" + } + } + }, + "safe-buffer": { + "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" + }, + "serve-index-75lb": { + "version": "https://registry.npmjs.org/serve-index-75lb/-/serve-index-75lb-1.8.1.tgz", + "integrity": "sha1-WSn3VtseB5bvfHhemB3Tniaw78I=", + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", + "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=" + }, + "http-errors": { + "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", + "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=" + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "setprototypeof": { + "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", + "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=" + } + } + }, + "setprototypeof": { + "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "statuses": { + "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "streaming-json-stringify": { + "version": "https://registry.npmjs.org/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz", + "integrity": "sha1-gCAEN6mTzDnE/gAmO3s7kDrIevU=" + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=" + }, + "test-runner": { + "version": "https://registry.npmjs.org/test-runner/-/test-runner-0.3.0.tgz", + "integrity": "sha1-0cmKEdFaA1vabvI0KhBOcCUajVg=", + "dev": true + }, + "test-value": { + "version": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", + "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=" + }, + "thenify": { + "version": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", + "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=" + }, + "thenify-all": { + "version": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=" + }, + "type-is": { + "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" + }, + "typical": { + "version": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", + "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" + }, + "unpipe": { + "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "wordwrapjs": { + "version": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-2.0.0.tgz", + "integrity": "sha1-q1X2leYRjak4WP3XDAU9HF4BrCA=", + "dev": true + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 78fcf7c..c0620e1 100644 --- a/package.json +++ b/package.json @@ -37,20 +37,20 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { + "lws": "^1.0.0-pre.4", "lws-blacklist": "^0.1.0", "lws-body-parser": "^0.1.0", "lws-compress": "^0.1.0", "lws-conditional-get": "^0.1.0", - "lws-cors": "^0.1.0", - "lws-index": "^0.1.1", - "lws-json": "^0.1.0", - "lws-log": "^0.1.1", + "lws-cors": "^0.2.0", + "lws-index": "^0.2.0", + "lws-json": "^0.2.0", + "lws-log": "^0.2.0", "lws-mime": "^0.1.0", "lws-mock-response": "^0.1.0", - "lws-rewrite": "^0.1.0", + "lws-rewrite": "^0.2.0", "lws-spa": "^0.1.2", - "lws-static": "^0.1.1", - "lws": "^1.0.0-pre.4" + "lws-static": "^0.2.0" }, "devDependencies": { "test-runner": "^0.3.0" From b7289201e1ca4bbd73470798a58f0d8ebbab1b48 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 10 Jun 2017 21:16:08 +0100 Subject: [PATCH 074/136] refactor bin run --- bin/cli.js | 14 +------------- lib/local-web-server.js | 41 ++++++++++++++++++++--------------------- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index fce734c..b3eed89 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,14 +1,2 @@ #!/usr/bin/env node -'use strict' -const LocalWebServer = require('../') -const localWebServer = new LocalWebServer() -try { - localWebServer.start() -} catch (err) { - if (err.code === 'MODULE_NOT_FOUND') { - console.error(err.message) - console.error(require('util').inspect(err.attempted, { depth: 6, colors: true })) - } else { - console.error(err.stack) - } -} +require('../').run() diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 1d5ad99..dc80d4e 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,6 +1,7 @@ 'use strict' const Lws = require('lws') -const Serve = require('lws/lib/command/serve/serve') +const Serve = require('lws/lib/command/serve') +const path = require('path') /** * @module local-web-server @@ -8,26 +9,23 @@ const Serve = require('lws/lib/command/serve/serve') class WsServe extends Serve { execute (options, argv) { - const path = require('path') - let stack = [ - 'lws-log', - 'lws-cors', - 'lws-json', - 'lws-rewrite', - 'lws-body-parser', - 'lws-blacklist', - 'lws-conditional-get', - 'lws-mime', - 'lws-compress', - 'lws-mock-response', - 'lws-spa', - 'lws-static', - 'lws-index' - ] - const moduleDir = path.resolve(__dirname, `../node_modules`) options = { - stack, - moduleDir, + stack: [ + 'lws-log', + 'lws-cors', + 'lws-json', + 'lws-rewrite', + 'lws-body-parser', + 'lws-blacklist', + 'lws-conditional-get', + 'lws-mime', + 'lws-compress', + 'lws-mock-response', + 'lws-spa', + 'lws-static', + 'lws-index' + ], + moduleDir: path.resolve(__dirname, `../node_modules`), modulePrefix: 'lws-' } super.execute(options, argv) @@ -64,12 +62,13 @@ class WsServe extends Serve { class LocalWebServer extends Lws { constructor (options) { super (options) + /* override default serve command */ this.commands.add(null, WsServe) + /* add feature-list command */ this.commands.add('feature-list', require('./feature-list')) } getVersion () { - const path = require('path') const pkg = require(path.resolve(__dirname, '..', 'package.json')) return pkg.version } From 498ee868c07dc14aa4b6ef070e3e91925b0119f4 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 10 Jun 2017 23:27:10 +0100 Subject: [PATCH 075/136] --version --- lib/local-web-server.js | 10 +- package-lock.json | 1352 ----------------------------------------------- yarn.lock | 879 ++++++++++++++++++++++++++++++ 3 files changed, 884 insertions(+), 1357 deletions(-) delete mode 100644 package-lock.json create mode 100644 yarn.lock diff --git a/lib/local-web-server.js b/lib/local-web-server.js index dc80d4e..cfe7c88 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -54,6 +54,11 @@ class WsServe extends Serve { }) return sections } + + showVersion () { + const pkg = require(path.resolve(__dirname, '..', 'package.json')) + console.log(pkg.version) + } } /** @@ -67,11 +72,6 @@ class LocalWebServer extends Lws { /* add feature-list command */ this.commands.add('feature-list', require('./feature-list')) } - - getVersion () { - const pkg = require(path.resolve(__dirname, '..', 'package.json')) - return pkg.version - } } module.exports = LocalWebServer diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 10c22c5..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1352 +0,0 @@ -{ - "name": "local-web-server", - "version": "2.0.0-pre.1", - "lockfileVersion": 1, - "dependencies": { - "accepts": { - "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", - "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" - }, - "ansi-escape-sequences": { - "version": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz", - "integrity": "sha1-HBg5S2r5t2/5pjUJ+kl2af0s5T4=", - "dev": true - }, - "any-promise": { - "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" - }, - "array-back": { - "version": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=" - }, - "balanced-match": { - "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", - "dev": true - }, - "batch": { - "version": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz", - "integrity": "sha1-PzQU84AyF0O/wQQvmoP/HVgk1GQ=" - }, - "brace-expansion": { - "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", - "dev": true - }, - "buffer-shims": { - "version": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, - "bytes": { - "version": "https://registry.npmjs.org/bytes/-/bytes-2.5.0.tgz", - "integrity": "sha1-TJQj6i0lLCcMQbK97+/5u2tiwGo=" - }, - "co": { - "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "co-body": { - "version": "https://registry.npmjs.org/co-body/-/co-body-5.1.1.tgz", - "integrity": "sha1-2XeB0eM0S6SoIP0YBr3fg0FQUjY=" - }, - "command-line-tool": { - "version": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.6.4.tgz", - "integrity": "sha1-TBHjcvPkElSGHD/mtTjTx6WxRPM=", - "dev": true, - "dependencies": { - "command-line-args": { - "version": "https://registry.npmjs.org/command-line-args/-/command-line-args-3.0.5.tgz", - "integrity": "sha1-W9StReeYPlwTRJGOQCgO4mk8WsA=", - "dev": true - }, - "command-line-usage": { - "version": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-3.0.8.tgz", - "integrity": "sha1-tqIJeMGzg0d/XBGlKUKLiAv+D00=", - "dev": true - }, - "table-layout": { - "version": "https://registry.npmjs.org/table-layout/-/table-layout-0.3.0.tgz", - "integrity": "sha1-buINxIPbNxs+XIf3BO0vfHmdLJo=", - "dev": true - } - } - }, - "compressible": { - "version": "https://registry.npmjs.org/compressible/-/compressible-2.0.10.tgz", - "integrity": "sha1-/tocf3YXkScyspv4zyYlKiC57s0=" - }, - "concat-map": { - "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "copy-to": { - "version": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", - "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" - }, - "core-js": { - "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", - "dev": true - }, - "core-util-is": { - "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dependencies": { - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "deep-extend": { - "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "dev": true - }, - "defer-promise": { - "version": "https://registry.npmjs.org/defer-promise/-/defer-promise-1.0.1.tgz", - "integrity": "sha1-HKb/7dvO8XFd16riXHYW+a4iky8=" - }, - "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", - "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" - }, - "escape-html": { - "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", - "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" - }, - "feature-detect-es6": { - "version": "https://registry.npmjs.org/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz", - "integrity": "sha1-+IhzavnLDJH1VmO/pHYuuW7nBH8=", - "dev": true - }, - "file-set": { - "version": "https://registry.npmjs.org/file-set/-/file-set-1.1.1.tgz", - "integrity": "sha1-0+xwwIDsjxjyBLod4QZ4DJBWkms=", - "dev": true - }, - "find-replace": { - "version": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", - "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=", - "dev": true - }, - "fs.realpath": { - "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", - "dev": true - }, - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", - "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=" - }, - "iconv-lite": { - "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" - }, - "inflation": { - "version": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", - "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" - }, - "inflight": { - "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "json-stringify-safe": { - "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "koa-bodyparser": { - "version": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.2.0.tgz", - "integrity": "sha1-vObgi8Zfhwm20fqpQRx/DYk4qlQ=" - }, - "koa-compress": { - "version": "https://registry.npmjs.org/koa-compress/-/koa-compress-2.0.0.tgz", - "integrity": "sha1-e36ykhuEd0a14SK6n1zYpnHo6jo=" - }, - "koa-conditional-get": { - "version": "https://registry.npmjs.org/koa-conditional-get/-/koa-conditional-get-2.0.0.tgz", - "integrity": "sha1-pD83I8HQFLcwo07Oit8wuTyCM/I=" - }, - "koa-etag": { - "version": "https://registry.npmjs.org/koa-etag/-/koa-etag-3.0.0.tgz", - "integrity": "sha1-nvc4Ld1agqsN6xU0FckVg293HT8=" - }, - "koa-is-json": { - "version": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", - "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" - }, - "koa-json": { - "version": "https://registry.npmjs.org/koa-json/-/koa-json-2.0.2.tgz", - "integrity": "sha1-Nq8U5uofXWRtfESihXAcb4Wk/eQ=" - }, - "koa-mock-response": { - "version": "https://registry.npmjs.org/koa-mock-response/-/koa-mock-response-0.0.2.tgz", - "integrity": "sha1-/CnpoHvQva61p5oQB/NxQjRgsQ0=" - }, - "koa-rewrite": { - "version": "https://registry.npmjs.org/koa-rewrite/-/koa-rewrite-2.1.0.tgz", - "integrity": "sha1-gSs19IqGADG8hi+IE15cKDGLeqg=", - "dependencies": { - "path-to-regexp": { - "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.0.2.tgz", - "integrity": "sha1-SJ/rBgsxREOlSUqx2i7+0gQKskw=" - } - } - }, - "koa-route": { - "version": "https://registry.npmjs.org/koa-route/-/koa-route-3.2.0.tgz", - "integrity": "sha1-dimLmaa8+p44yrb+XHmocz51i84=" - }, - "koa-send": { - "version": "https://registry.npmjs.org/koa-send/-/koa-send-3.3.0.tgz", - "integrity": "sha1-WkriRVZGgMbs9geeknX6UXOoYdw=" - }, - "koa-static": { - "version": "https://registry.npmjs.org/koa-static/-/koa-static-3.0.0.tgz", - "integrity": "sha1-QEQiM9LAs1wiXkUBmcEL+KU55BY=" - }, - "lodash.pick": { - "version": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" - }, - "lws": { - "version": "1.0.0-pre.4", - "dependencies": { - "accepts": { - "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", - "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" - }, - "ansi-escape-sequences": { - "version": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz", - "integrity": "sha1-HBg5S2r5t2/5pjUJ+kl2af0s5T4=" - }, - "ansi-regex": { - "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "any-promise": { - "version": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" - }, - "argparse": { - "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=" - }, - "array-back": { - "version": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=" - }, - "array-base": { - "version": "https://registry.npmjs.org/array-base/-/array-base-1.0.1.tgz", - "integrity": "sha1-6hM7DBLCX+MzCraELJmSd3Ghuvo=" - }, - "asn1": { - "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" - }, - "asynckit": { - "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws-sign2": { - "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "aws4": { - "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "balanced-match": { - "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" - }, - "bcrypt-pbkdf": { - "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true - }, - "boom": { - "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=" - }, - "brace-expansion": { - "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=" - }, - "byte-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-3.0.0.tgz", - "integrity": "sha1-QG+eI2aqXav2NnLrKR17sJSV2nU=" - }, - "caseless": { - "version": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - }, - "chalk": { - "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=" - }, - "cli-commands": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-commands/-/cli-commands-0.3.1.tgz", - "integrity": "sha512-UloGvUiUEelYX8LlTORQaZwm2lOMR0goS+Ga+RlkLpoCpGv5QnwGsrwBO8UF0ZULdvuWsXNJqiOnAPGh9LyTjw==" - }, - "co": { - "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "combined-stream": { - "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=" - }, - "command-line-args": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-4.0.6.tgz", - "integrity": "sha1-D/h6HdFZiQ3K6yoAWr2uceVQWfw=" - }, - "command-line-commands": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/command-line-commands/-/command-line-commands-2.0.0.tgz", - "integrity": "sha1-Gr2hFjPo03vLGjn3O42Tmi2F7zk=" - }, - "command-line-tool": { - "version": "https://registry.npmjs.org/command-line-tool/-/command-line-tool-0.6.4.tgz", - "integrity": "sha1-TBHjcvPkElSGHD/mtTjTx6WxRPM=", - "dependencies": { - "command-line-args": { - "version": "https://registry.npmjs.org/command-line-args/-/command-line-args-3.0.5.tgz", - "integrity": "sha1-W9StReeYPlwTRJGOQCgO4mk8WsA=" - }, - "command-line-usage": { - "version": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-3.0.8.tgz", - "integrity": "sha1-tqIJeMGzg0d/XBGlKUKLiAv+D00=" - }, - "table-layout": { - "version": "https://registry.npmjs.org/table-layout/-/table-layout-0.3.0.tgz", - "integrity": "sha1-buINxIPbNxs+XIf3BO0vfHmdLJo=" - } - } - }, - "command-line-usage": { - "version": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-4.0.0.tgz", - "integrity": "sha1-gWsyeItY+f66RNHm2sYPyuspteo=" - }, - "commander": { - "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=" - }, - "concat-map": { - "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "content-disposition": { - "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", - "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" - }, - "cookies": { - "version": "https://registry.npmjs.org/cookies/-/cookies-0.7.0.tgz", - "integrity": "sha1-C8lh2RDDUlSYD8fJ7/XaEgEbvwA=" - }, - "core-js": { - "version": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "coveralls": { - "version": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.1.tgz", - "integrity": "sha1-1wu5rMGDXsTwY/+drFQjwXsR8Xg=" - }, - "cryptiles": { - "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=" - }, - "dashdash": { - "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" - }, - "deep-equal": { - "version": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-extend": { - "version": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, - "defer-promise": { - "version": "https://registry.npmjs.org/defer-promise/-/defer-promise-1.0.1.tgz", - "integrity": "sha1-HKb/7dvO8XFd16riXHYW+a4iky8=" - }, - "delayed-stream": { - "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", - "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" - }, - "destroy": { - "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-node": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", - "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=" - }, - "ecc-jsbn": { - "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true - }, - "ee-first": { - "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "error-inject": { - "version": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", - "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" - }, - "escape-html": { - "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "esprima": { - "version": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "extend": { - "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extsprintf": { - "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" - }, - "feature-detect-es6": { - "version": "https://registry.npmjs.org/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz", - "integrity": "sha1-+IhzavnLDJH1VmO/pHYuuW7nBH8=" - }, - "file-set": { - "version": "https://registry.npmjs.org/file-set/-/file-set-1.1.1.tgz", - "integrity": "sha1-0+xwwIDsjxjyBLod4QZ4DJBWkms=" - }, - "find-replace": { - "version": "https://registry.npmjs.org/find-replace/-/find-replace-1.0.3.tgz", - "integrity": "sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=" - }, - "forever-agent": { - "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=" - }, - "fresh": { - "version": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", - "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" - }, - "fs.realpath": { - "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "generate-function": { - "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" - }, - "generate-object-property": { - "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=" - }, - "getpass": { - "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "glob": { - "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=" - }, - "graceful-readlink": { - "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, - "handle-thing": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=" - }, - "har-validator": { - "version": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=" - }, - "has-ansi": { - "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=" - }, - "hawk": { - "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=" - }, - "hoek": { - "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=" - }, - "http-assert": { - "version": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", - "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=" - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" - }, - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", - "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=", - "dependencies": { - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "http-signature": { - "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=" - }, - "http2": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/http2/-/http2-3.3.6.tgz", - "integrity": "sha1-ffBiJ+ArW1pYQd7qCCObMZjQS+w=" - }, - "inflight": { - "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "is-generator-function": { - "version": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.6.tgz", - "integrity": "sha1-nnFlPNFf/zQcecQVFGChMdMen8Q=" - }, - "is-my-json-valid": { - "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", - "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=" - }, - "is-property": { - "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" - }, - "is-typedarray": { - "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isstream": { - "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jodid25519": { - "version": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", - "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", - "optional": true - }, - "js-yaml": { - "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", - "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=" - }, - "jsbn": { - "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-stringify-safe": { - "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonpointer": { - "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" - }, - "jsprim": { - "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", - "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "keygrip": { - "version": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.1.tgz", - "integrity": "sha1-sC+kgW7vIajEs1yp5Skh/8iaMOk=" - }, - "koa": { - "version": "https://registry.npmjs.org/koa/-/koa-2.2.0.tgz", - "integrity": "sha1-sFWTMYeEnVQK2Ln3MbqqS+l8ZS0=" - }, - "koa-compose": { - "version": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", - "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=" - }, - "koa-convert": { - "version": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", - "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=" - }, - "koa-is-json": { - "version": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", - "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" - }, - "lcov-parse": { - "version": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=" - }, - "load-module": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/load-module/-/load-module-0.2.0.tgz", - "integrity": "sha512-u7vn0QrCaFlCg3/ehUAyMpf4cuzonoU0Rj1fDT2Fk9xD6EvQF7eoVJlLqeshiUtwZAYqS47SC4HAFnhkeVI3ZQ==" - }, - "lodash.assignwith": { - "version": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", - "integrity": "sha1-EnqX8CrcQXUalU0ksN4X4QDgOOs=" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, - "lodash.padend": { - "version": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" - }, - "lodash.pick": { - "version": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" - }, - "log-driver": { - "version": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", - "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=" - }, - "media-typer": { - "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" - }, - "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" - }, - "minimalistic-assert": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", - "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" - }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=" - }, - "minimist": { - "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "oauth-sign": { - "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "obuf": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz", - "integrity": "sha1-EEEktsYCxnlogaBCVB0220OlJk4=" - }, - "on-finished": { - "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" - }, - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" - }, - "only": { - "version": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" - }, - "parseurl": { - "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", - "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" - }, - "path-is-absolute": { - "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "pinkie": { - "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=" - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=" - }, - "readable-stream": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.11.tgz", - "integrity": "sha512-h+8+r3MKEhkiVrwdKL8aWs1oc1VvBu33ueshOvS26RsZQ3Amhx/oO3TKe4lApSV9ueY6as8EAh7mtuFjdlhg9Q==", - "dependencies": { - "safe-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" - } - } - }, - "reduce-flatten": { - "version": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" - }, - "req-then": { - "version": "https://registry.npmjs.org/req-then/-/req-then-0.6.1.tgz", - "integrity": "sha1-6Fr2uTn/zEtPFFt+UcrnPo+AUsk=" - }, - "request": { - "version": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=" - }, - "safe-buffer": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.0.tgz", - "integrity": "sha512-aSLEDudu6OoRr/2rU609gRmnYboRLxgDG1z9o2Q0os7236FwvcqIOO8r8U5JUEwivZOhDaKlFO4SbPTJYyBEyQ==" - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=" - }, - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "sntp": { - "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=" - }, - "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=" - }, - "spdy-transport": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.0.20.tgz", - "integrity": "sha1-c15yBUxIayNU/onnAiVgBKOazk0=" - }, - "sprintf-js": { - "version": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", - "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", - "dependencies": { - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "string_decoder": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", - "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=" - }, - "stringstream": { - "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=" - }, - "supports-color": { - "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "table-layout": { - "version": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.0.tgz", - "integrity": "sha1-xw/wRV2a3WO5H3wVp3kmKVwODn0=" - }, - "test-runner": { - "version": "https://registry.npmjs.org/test-runner/-/test-runner-0.3.0.tgz", - "integrity": "sha1-0cmKEdFaA1vabvI0KhBOcCUajVg=" - }, - "test-value": { - "version": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=" - }, - "tough-cookie": { - "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=" - }, - "tunnel-agent": { - "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" - }, - "tweetnacl": { - "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-is": { - "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" - }, - "typical": { - "version": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" - }, - "vary": { - "version": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", - "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" - }, - "verror": { - "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=" - }, - "walk-back": { - "version": "https://registry.npmjs.org/walk-back/-/walk-back-3.0.0.tgz", - "integrity": "sha1-I1h4ejXakQMtrV6S+AsSNw2HlcU=" - }, - "wbuf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.2.tgz", - "integrity": "sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=" - }, - "wordwrapjs": { - "version": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-2.0.0.tgz", - "integrity": "sha1-q1X2leYRjak4WP3XDAU9HF4BrCA=" - }, - "wrappy": { - "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - } - } - }, - "lws-blacklist": { - "version": "https://registry.npmjs.org/lws-blacklist/-/lws-blacklist-0.1.0.tgz", - "integrity": "sha1-LmSXREwA+mRiSNtziYZ3Y9WrWjo=" - }, - "lws-body-parser": { - "version": "https://registry.npmjs.org/lws-body-parser/-/lws-body-parser-0.1.0.tgz", - "integrity": "sha1-Ge/CamCCT1C59h+8Cn6CfIunvGU=" - }, - "lws-compress": { - "version": "https://registry.npmjs.org/lws-compress/-/lws-compress-0.1.0.tgz", - "integrity": "sha1-wuaNffpTReb00ZEZU1xpaF6AkJQ=" - }, - "lws-conditional-get": { - "version": "https://registry.npmjs.org/lws-conditional-get/-/lws-conditional-get-0.1.0.tgz", - "integrity": "sha1-UCX2Vf7W4oXA7+gQPkPp51EySpI=" - }, - "lws-cors": { - "version": "0.2.0", - "dependencies": { - "kcors": { - "version": "https://registry.npmjs.org/kcors/-/kcors-2.2.1.tgz", - "integrity": "sha1-cWCpTy6uYzQ20s746t0M4jI4Z3k=" - } - } - }, - "lws-index": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/lws-index/-/lws-index-0.2.0.tgz", - "integrity": "sha512-8+ezEKIOiTbcn1VWa0v7X0AP1JpoTaywVSL1YOrf41FQl4P5ibzg64jZAPBurX7CL5qjZbU+o/SqyFvkVgur5g==" - }, - "lws-json": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/lws-json/-/lws-json-0.2.0.tgz", - "integrity": "sha512-VOaVZj3waaH2+bm+M5S3vneENYlnkxkyn+usj6RU5tuiSEzCodSnK3cCtq/2FD0/WZt82i7s0mhjG7m2/akEnA==" - }, - "lws-log": { - "version": "0.2.0", - "dependencies": { - "ansi-escape-sequences": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz", - "integrity": "sha1-HBg5S2r5t2/5pjUJ+kl2af0s5T4=" - }, - "array-back": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-1.0.4.tgz", - "integrity": "sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=" - }, - "basic-auth": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", - "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=" - }, - "byte-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-3.0.0.tgz", - "integrity": "sha1-QG+eI2aqXav2NnLrKR17sJSV2nU=" - }, - "common-log-format": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/common-log-format/-/common-log-format-0.1.3.tgz", - "integrity": "sha1-YAna3E7EZzca8oqNvT5M4CsWZ4I=" - }, - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, - "depd": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", - "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "feature-detect-es6": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz", - "integrity": "sha1-+IhzavnLDJH1VmO/pHYuuW7nBH8=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "JSONStream": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", - "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=" - }, - "koa-morgan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", - "integrity": "sha1-CAUuDODYOdPEMXi5CluzQkvvH5k=" - }, - "lodash.padend": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=" - }, - "lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" - }, - "morgan": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.8.2.tgz", - "integrity": "sha1-eErHc05KRTqcbm6GgKkyknXItoc=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" - }, - "reduce-flatten": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" - }, - "stream-log-stats": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-log-stats/-/stream-log-stats-2.0.2.tgz", - "integrity": "sha512-b1LccxXhMlOQQrzSqapQHyZ3UI00QTAv+8VecFgsJz//sGB5LFl/+mkFeWBVVI2/E4DlCT4sGgvLExB/VTVFfA==" - }, - "stream-via": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-via/-/stream-via-1.0.4.tgz", - "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==" - }, - "table-layout": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-0.4.0.tgz", - "integrity": "sha1-xw/wRV2a3WO5H3wVp3kmKVwODn0=" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "typical": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" - }, - "wordwrapjs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-2.0.0.tgz", - "integrity": "sha1-q1X2leYRjak4WP3XDAU9HF4BrCA=" - } - } - }, - "lws-mime": { - "version": "https://registry.npmjs.org/lws-mime/-/lws-mime-0.1.0.tgz", - "integrity": "sha1-m2wpzSZkDWHw2oEPvfIiTij7GU8=" - }, - "lws-mock-response": { - "version": "https://registry.npmjs.org/lws-mock-response/-/lws-mock-response-0.1.0.tgz", - "integrity": "sha1-Kucq7xyekOxLq0d3D7gTRtfV4vU=" - }, - "lws-rewrite": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/lws-rewrite/-/lws-rewrite-0.2.0.tgz", - "integrity": "sha512-DVw/FxIyAZWXTxiPGHH6yql2HnbWCUFK3ZpgSOpZ8Yzw7dxBbThW8vob2oAq3rIo8JGak0OWMmeLd9epvnvNrA==" - }, - "lws-spa": { - "version": "https://registry.npmjs.org/lws-spa/-/lws-spa-0.1.2.tgz", - "integrity": "sha1-l/2p1eG1vOyhpFWBdWVRkLEV6P4=", - "dependencies": { - "koa-send": { - "version": "https://registry.npmjs.org/koa-send/-/koa-send-4.1.0.tgz", - "integrity": "sha1-B9Wk6qshJnn+mZFqrmsRCcCMI2E=" - } - } - }, - "lws-static": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/lws-static/-/lws-static-0.2.0.tgz", - "integrity": "sha512-s/tin0Z4Bkg5O2vY8orJUhtPZWDSe/gTfMpyai4EGy1xn2kJ2gM6iM/auT1wysdVIxe9lCTN7iqBJlkn400l6g==" - }, - "media-typer": { - "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "methods": { - "version": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.28.0.tgz", - "integrity": "sha1-/t00m+BtKGW3/FfYN8beTxfXrDw=" - }, - "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", - "dependencies": { - "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" - } - } - }, - "minimatch": { - "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "dev": true - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" - }, - "mz": { - "version": "https://registry.npmjs.org/mz/-/mz-2.6.0.tgz", - "integrity": "sha1-yLhSHZWN8KTydoAl22nHGe5O8c4=" - }, - "negotiator": { - "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "object-assign": { - "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true - }, - "parseurl": { - "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", - "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" - }, - "path-is-absolute": { - "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-to-regexp": { - "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=" - }, - "process-nextick-args": { - "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "raw-body": { - "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", - "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=", - "dependencies": { - "bytes": { - "version": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", - "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" - } - } - }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", - "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", - "dependencies": { - "isarray": { - "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - } - } - }, - "reduce-flatten": { - "version": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", - "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=", - "dev": true - }, - "req-then": { - "version": "https://registry.npmjs.org/req-then/-/req-then-0.6.1.tgz", - "integrity": "sha1-6Fr2uTn/zEtPFFt+UcrnPo+AUsk=" - }, - "resolve-path": { - "version": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.3.3.tgz", - "integrity": "sha1-TYOrpkaMK45jKldeP1Kw+g2+Glw=", - "dependencies": { - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", - "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=" - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", - "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=" - } - } - }, - "safe-buffer": { - "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", - "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" - }, - "serve-index-75lb": { - "version": "https://registry.npmjs.org/serve-index-75lb/-/serve-index-75lb-1.8.1.tgz", - "integrity": "sha1-WSn3VtseB5bvfHhemB3Tniaw78I=", - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", - "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=" - }, - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", - "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=" - }, - "inherits": { - "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", - "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=" - } - } - }, - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "streaming-json-stringify": { - "version": "https://registry.npmjs.org/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz", - "integrity": "sha1-gCAEN6mTzDnE/gAmO3s7kDrIevU=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", - "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=" - }, - "test-runner": { - "version": "https://registry.npmjs.org/test-runner/-/test-runner-0.3.0.tgz", - "integrity": "sha1-0cmKEdFaA1vabvI0KhBOcCUajVg=", - "dev": true - }, - "test-value": { - "version": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", - "integrity": "sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=" - }, - "thenify": { - "version": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=" - }, - "thenify-all": { - "version": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=" - }, - "type-is": { - "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" - }, - "typical": { - "version": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", - "integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=" - }, - "unpipe": { - "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "util-deprecate": { - "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "wordwrapjs": { - "version": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-2.0.0.tgz", - "integrity": "sha1-q1X2leYRjak4WP3XDAU9HF4BrCA=", - "dev": true - }, - "wrappy": { - "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } -} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..de42545 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,879 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +JSONStream@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +accepts@^1.2.2, accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +ansi-escape-sequences@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz#1c18394b6af9b76ff9a63509fa497669fd2ce53e" + dependencies: + array-back "^1.0.3" + +any-promise@^1.0.0, any-promise@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + +array-back@^1.0.3, array-back@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b" + dependencies: + typical "^2.6.0" + +balanced-match@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +basic-auth@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" + +batch@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464" + +brace-expansion@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" + dependencies: + balanced-match "^0.4.1" + concat-map "0.0.1" + +byte-size@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-3.0.0.tgz#406f9e2366aa5dabf63672eb291d7bb09495da75" + +bytes@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" + +bytes@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" + +co-body@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.1.1.tgz#d97781d1e3344ba4a820fd1806bddf8341505236" + dependencies: + inflation "^2.0.0" + qs "^6.4.0" + raw-body "^2.2.0" + type-is "^1.6.14" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +command-line-args@^3.0.1: + version "3.0.5" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-3.0.5.tgz#5bd4ad45e7983e5c1344918e40280ee2693c5ac0" + dependencies: + array-back "^1.0.4" + feature-detect-es6 "^1.3.1" + find-replace "^1.0.2" + typical "^2.6.0" + +command-line-args@^4.0.2: + version "4.0.6" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-4.0.6.tgz#0ff87a1dd159890dcaeb2a005abdae71e55059fc" + dependencies: + array-back "^1.0.4" + find-replace "^1.0.3" + typical "^2.6.1" + +command-line-tool@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/command-line-tool/-/command-line-tool-0.6.4.tgz#4c11e372f3e41254861c3fe6b538d3c7a5b144f3" + dependencies: + ansi-escape-sequences "^3.0.0" + array-back "^1.0.3" + command-line-args "^3.0.1" + command-line-usage "^3.0.3" + feature-detect-es6 "^1.3.1" + typical "^2.6.0" + +command-line-usage@^3.0.3: + version "3.0.8" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-3.0.8.tgz#b6a20978c1b383477f5c11a529428b880bfe0f4d" + dependencies: + ansi-escape-sequences "^3.0.0" + array-back "^1.0.3" + feature-detect-es6 "^1.3.1" + table-layout "^0.3.0" + typical "^2.6.0" + +command-line-usage@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-4.0.0.tgz#816b32788b58f9feba44d1e6dac60fcaeb29b5ea" + dependencies: + ansi-escape-sequences "^3.0.0" + array-back "^1.0.4" + table-layout "^0.4.0" + typical "^2.6.0" + +common-log-format@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/common-log-format/-/common-log-format-0.1.3.tgz#6009dadc4ec467371af28a8dbd3e4ce02b166782" + +compressible@^2.0.0: + version "2.0.10" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" + dependencies: + mime-db ">= 1.27.0 < 2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +content-disposition@~0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +cookies@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.0.tgz#0bc961d910c35254980fc7c9eff5da12011bbf00" + dependencies: + depd "~1.1.0" + keygrip "~1.0.1" + +copy-to@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" + +core-js@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +debug@*, debug@2.6.8, debug@^2.6.0, debug@^2.6.3: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" + dependencies: + ms "0.7.2" + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +defer-promise@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/defer-promise/-/defer-promise-1.0.1.tgz#1ca6ffeddbcef1715dd7aae25c7616f9ae22932f" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@1.1.0, depd@^1.1.0, depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +destroy@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +error-inject@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + +escape-html@~1.0.1, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +etag@^1.3.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" + +feature-detect-es6@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz#f888736af9cb0c91f55663bfa4762eb96ee7047f" + dependencies: + array-back "^1.0.3" + +file-set@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/file-set/-/file-set-1.1.1.tgz#d3ec70c080ec8f18f204ba1de106780c9056926b" + dependencies: + array-back "^1.0.3" + glob "^7.1.0" + +find-replace@^1.0.2, find-replace@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0" + dependencies: + array-back "^1.0.4" + test-value "^2.1.0" + +fresh@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +glob@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +http-assert@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a" + dependencies: + deep-equal "~1.0.1" + http-errors "~1.6.1" + +http-errors@^1.2.8, http-errors@^1.6.1, http-errors@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" + dependencies: + depd "1.1.0" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-errors@~1.5.0, http-errors@~1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" + dependencies: + inherits "2.0.3" + setprototypeof "1.0.2" + statuses ">= 1.3.1 < 2" + +iconv-lite@0.4.15: + version "0.4.15" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" + +inflation@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@~2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-generator-function@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.6.tgz#9e71653cd15fff341c79c4151460a131d31e9fc4" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +json-stringify-safe@5: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + +kcors@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/kcors/-/kcors-2.2.1.tgz#7160a94f2eae633436d2cef8eadd0ce232386779" + +keygrip@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.1.tgz#b02fa4816eef21a8c4b35ca9e52921ffc89a30e9" + +koa-bodyparser@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.2.0.tgz#bce6e08bc65f8709b6d1faa9411c7f0d8938aa54" + dependencies: + co-body "^5.1.0" + copy-to "^2.0.1" + +koa-compose@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + dependencies: + any-promise "^1.1.0" + +koa-compress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-2.0.0.tgz#7b7eb2921b847746b5e122ba9f5cd8a671e8ea3a" + dependencies: + bytes "^2.3.0" + compressible "^2.0.0" + koa-is-json "^1.0.0" + statuses "^1.0.0" + +koa-conditional-get@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/koa-conditional-get/-/koa-conditional-get-2.0.0.tgz#a43f3723c1d014b730a34ece8adf30b93c8233f2" + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa-etag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-3.0.0.tgz#9ef7382ddd5a82ab0deb153415c915836f771d3f" + dependencies: + etag "^1.3.0" + mz "^2.1.0" + +koa-is-json@1, koa-is-json@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" + +koa-json@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/koa-json/-/koa-json-2.0.2.tgz#36af14e6ea1f5d646d7c44a285701c6f85a4fde4" + dependencies: + koa-is-json "1" + streaming-json-stringify "3" + +koa-mock-response@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/koa-mock-response/-/koa-mock-response-0.0.2.tgz#fc29e9a07bd0bdaeb5a79a1007f371423460b10d" + dependencies: + array-back "^1.0.3" + path-to-regexp "^1.5.3" + test-value "^2.0.0" + typical "^2.4.2" + +koa-morgan@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/koa-morgan/-/koa-morgan-1.0.1.tgz#08052e0ce0d839d3c43178b90a5bb3424bef1f99" + dependencies: + morgan "^1.6.1" + +koa-rewrite@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/koa-rewrite/-/koa-rewrite-2.1.0.tgz#812b35f48a860031bc862f88135e5c28318b7aa8" + dependencies: + debug "*" + path-to-regexp "0.0.2" + +koa-route@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/koa-route/-/koa-route-3.2.0.tgz#76298b99a6bcfa9e38cab6fe5c79a8733e758bce" + dependencies: + debug "*" + methods "~1.1.0" + path-to-regexp "^1.2.0" + +koa-send@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-3.3.0.tgz#5a4ae245564680c6ecf6079e9275fa5173a861dc" + dependencies: + co "^4.6.0" + debug "^2.6.0" + mz "^2.3.1" + resolve-path "^1.3.1" + +koa-send@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-4.1.0.tgz#07d5a4eaab212679fe99916aae6b1109c08c2361" + dependencies: + debug "^2.6.3" + http-errors "^1.6.1" + mz "^2.6.0" + resolve-path "^1.3.3" + +koa-static@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-3.0.0.tgz#40442233d2c0b35c225e450199c10bf8a539e416" + dependencies: + debug "*" + koa-send "^3.2.0" + +koa@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.2.0.tgz#b055933187849d540ad8b9f731baaa4be97c652d" + dependencies: + accepts "^1.2.2" + content-disposition "~0.5.0" + content-type "^1.0.0" + cookies "~0.7.0" + debug "*" + delegates "^1.0.0" + depd "^1.1.0" + destroy "^1.0.3" + error-inject "~1.0.0" + escape-html "~1.0.1" + fresh "^0.5.0" + http-assert "^1.1.0" + http-errors "^1.2.8" + is-generator-function "^1.0.3" + koa-compose "^3.0.0" + koa-convert "^1.2.0" + koa-is-json "^1.0.0" + mime-types "^2.0.7" + on-finished "^2.1.0" + only "0.0.2" + parseurl "^1.3.0" + statuses "^1.2.0" + type-is "^1.5.5" + vary "^1.0.0" + +lodash.assignwith@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb" + +lodash.padend@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + +lws-blacklist@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lws-blacklist/-/lws-blacklist-0.1.0.tgz#2e6497444c00fa646248db7389867763d5ab5a3a" + dependencies: + array-back "^1.0.4" + path-to-regexp "^1.7.0" + +lws-body-parser@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lws-body-parser/-/lws-body-parser-0.1.0.tgz#19efc26a60824f50b9f61fbc0a7e827c8ba7bc65" + dependencies: + koa-bodyparser "^4.1.0" + +lws-compress@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lws-compress/-/lws-compress-0.1.0.tgz#c2e68d7dfa5345e6f4d19119535c69685e809094" + dependencies: + koa-compress "^2.0.0" + +lws-conditional-get@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lws-conditional-get/-/lws-conditional-get-0.1.0.tgz#5025f655fed6e285c0efe8103e43e9e751324a92" + dependencies: + koa-conditional-get "^2.0.0" + koa-etag "^3.0.0" + +lws-cors@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/lws-cors/-/lws-cors-0.2.0.tgz#de1c8863b1366d8a680fc4ad834b5b0709007cac" + dependencies: + kcors "^2.2.1" + +lws-index@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/lws-index/-/lws-index-0.2.0.tgz#ff6b109efa748d648e0c8a65956daa7fdb576e3d" + dependencies: + serve-index-75lb "^1.8.1" + +lws-json@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/lws-json/-/lws-json-0.2.0.tgz#8386206506ae0a4cfe6fc6d74b75b8f25ac7c17f" + dependencies: + koa-json "^2.0.2" + +lws-log@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/lws-log/-/lws-log-0.2.0.tgz#b744105c8983b4d0bbe89a0677970684007137f3" + dependencies: + koa-morgan "^1.0.1" + stream-log-stats "^2.0.2" + +lws-mime@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lws-mime/-/lws-mime-0.1.0.tgz#9b6c29cd26640d61f0da810fbdf2224e28fb194f" + +lws-mock-response@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lws-mock-response/-/lws-mock-response-0.1.0.tgz#2ae72aef1c9e90ec4bab47770fb81346d7d5e2f5" + dependencies: + array-back "^1.0.4" + koa-mock-response "0.0.2" + +lws-rewrite@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/lws-rewrite/-/lws-rewrite-0.2.1.tgz#5b15cb466237d72f2a9d24efcb6040d3f0729621" + dependencies: + array-back "^1.0.4" + koa-rewrite "^2.1.0" + koa-route "^3.2.0" + path-to-regexp "^1.7.0" + req-then "~0.6.1" + typical "^2.6.1" + +lws-spa@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/lws-spa/-/lws-spa-0.1.2.tgz#97fda9d5e1b5bceca1a4558175655190b115e8fe" + dependencies: + koa-route "^3.2.0" + koa-send "^4.1.0" + +lws-static@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/lws-static/-/lws-static-0.2.0.tgz#74df9be31ece6534894356fa7d884b53cf1075d4" + dependencies: + koa-static "^3.0.0" + +lws@^1.0.0-pre.4: + version "1.0.0-pre.4" + resolved "https://registry.yarnpkg.com/lws/-/lws-1.0.0-pre.4.tgz#d051a7919d1ff776cc39c1955f4ec48d4d0e2e7c" + dependencies: + ansi-escape-sequences "^3.0.0" + array-back "^1.0.4" + command-line-args "^4.0.2" + command-line-usage "^4.0.0" + koa "^2.2.0" + lodash.assignwith "^4.2.0" + reduce-flatten "^1.0.1" + typical "^2.6.0" + walk-back "^3.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +methods@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +"mime-db@>= 1.27.0 < 2": + version "1.28.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.28.0.tgz#fedd349be06d2865b7fc57d837c6de4f17d7ac3c" + +mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + +mime-types@^2.0.7, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.15: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + dependencies: + mime-db "~1.27.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +morgan@^1.6.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687" + dependencies: + basic-auth "~1.1.0" + debug "2.6.8" + depd "~1.1.0" + on-finished "~2.3.0" + on-headers "~1.0.1" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mz@^2.1.0, mz@^2.3.1, mz@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.6.0.tgz#c8b8521d958df0a4f2768025db69c719ee4ef1ce" + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +on-finished@^2.1.0, on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +only@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + +parseurl@^1.3.0, parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-is-absolute@1.0.1, path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-to-regexp@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.0.2.tgz#489feb060b314443a5494ab1da2efed2040ab24c" + +path-to-regexp@^1.2.0, path-to-regexp@^1.5.3, path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + +qs@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +raw-body@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" + dependencies: + bytes "2.4.0" + iconv-lite "0.4.15" + unpipe "1.0.0" + +readable-stream@2: + version "2.2.11" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + safe-buffer "~5.0.1" + string_decoder "~1.0.0" + util-deprecate "~1.0.1" + +reduce-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327" + +req-then@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/req-then/-/req-then-0.6.1.tgz#e85af6b939ffcc4b4f145b7e51cae73e8f8052c9" + dependencies: + array-back "^1.0.4" + defer-promise "^1.0.1" + lodash.pick "^4.4.0" + typical "^2.6.0" + +resolve-path@^1.3.1, resolve-path@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.3.3.tgz#4d83aba6468c2b8e632a575e3f52b0fa0dbe1a5c" + dependencies: + http-errors "~1.5.0" + path-is-absolute "1.0.1" + +safe-buffer@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" + +serve-index-75lb@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/serve-index-75lb/-/serve-index-75lb-1.8.1.tgz#5929f756db1e0796ef7c785e981dd39e26b0efc2" + dependencies: + accepts "~1.3.3" + batch "0.5.3" + debug "2.3.3" + escape-html "~1.0.3" + http-errors "~1.5.1" + mime-types "~2.1.13" + parseurl "~1.3.1" + +setprototypeof@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +"statuses@>= 1.3.1 < 2", statuses@^1.0.0, statuses@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stream-log-stats@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-log-stats/-/stream-log-stats-2.0.2.tgz#a59a4af9e224fa124457dd9b8ba89c757dc3dab0" + dependencies: + JSONStream "^1.3.1" + ansi-escape-sequences "^3.0.0" + byte-size "^3.0.0" + common-log-format "~0.1.3" + lodash.throttle "^4.1.1" + stream-via "^1.0.3" + table-layout "~0.4.0" + +stream-via@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stream-via/-/stream-via-1.0.4.tgz#8dccbb0ac909328eb8bc8e2a4bd3934afdaf606c" + +streaming-json-stringify@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz#80200437a993cc39c4fe00263b7b3b903ac87af5" + dependencies: + json-stringify-safe "5" + readable-stream "2" + +string_decoder@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.2.tgz#b29e1f4e1125fa97a10382b8a533737b7491e179" + dependencies: + safe-buffer "~5.0.1" + +table-layout@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.3.0.tgz#6ee20dc483db371b3e5c87f704ed2f7c799d2c9a" + dependencies: + array-back "^1.0.3" + core-js "^2.4.1" + deep-extend "~0.4.1" + feature-detect-es6 "^1.3.1" + typical "^2.6.0" + wordwrapjs "^2.0.0-0" + +table-layout@^0.4.0, table-layout@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.0.tgz#c70ff0455d9add63b91f7c15a77926295c0e0e7d" + dependencies: + array-back "^1.0.4" + deep-extend "~0.4.1" + lodash.padend "^4.6.1" + typical "^2.6.0" + wordwrapjs "^2.0.0" + +test-runner@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/test-runner/-/test-runner-0.3.0.tgz#d1c98a11d15a035bda6ef2342a104e70251a8d58" + dependencies: + ansi-escape-sequences "^3.0.0" + array-back "^1.0.3" + command-line-tool "~0.6.4" + core-js "^2.4.1" + feature-detect-es6 "^1.3.1" + file-set "^1.1.1" + reduce-flatten "^1.0.1" + +test-value@^2.0.0, test-value@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291" + dependencies: + array-back "^1.0.3" + typical "^2.6.0" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.0" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + dependencies: + any-promise "^1.0.0" + +"through@>=2.2.7 <3": + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +type-is@^1.5.5, type-is@^1.6.14: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +typical@^2.4.2, typical@^2.6.0, typical@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +vary@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" + +walk-back@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-3.0.0.tgz#2358787a35da91032dad5e92f80b12370d8795c5" + +wordwrapjs@^2.0.0, wordwrapjs@^2.0.0-0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-2.0.0.tgz#ab55f695e6118da93858fdd70c053d1c5e01ac20" + dependencies: + array-back "^1.0.3" + feature-detect-es6 "^1.3.1" + reduce-flatten "^1.0.1" + typical "^2.6.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" From d07ef41cd2594449cf669b267e50430302e2c312 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 11 Jun 2017 19:26:47 +0100 Subject: [PATCH 076/136] refactor cli --- bin/cli.js | 2 +- lib/cli-app.js | 17 +++++++++ lib/{ => command}/feature-list.js | 0 lib/command/serve.js | 62 +++++++++++++++++++++++++++++++ lib/local-web-server.js | 77 --------------------------------------- 5 files changed, 80 insertions(+), 78 deletions(-) create mode 100644 lib/cli-app.js rename lib/{ => command}/feature-list.js (100%) create mode 100644 lib/command/serve.js diff --git a/bin/cli.js b/bin/cli.js index b3eed89..c108e2e 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../').run() +require('../lib/cli-app').run() diff --git a/lib/cli-app.js b/lib/cli-app.js new file mode 100644 index 0000000..941003c --- /dev/null +++ b/lib/cli-app.js @@ -0,0 +1,17 @@ +'use strict' +const LwsCliApp = require('lws/lib/cli-app') + +/** + * @alias module:local-web-server + */ +class WsCliApp extends LwsCliApp { + constructor (options) { + super (options) + /* override default serve command */ + this.commands.add(null, require('./command/serve')) + /* add feature-list command */ + this.commands.add('feature-list', require('./command/feature-list')) + } +} + +module.exports = WsCliApp diff --git a/lib/feature-list.js b/lib/command/feature-list.js similarity index 100% rename from lib/feature-list.js rename to lib/command/feature-list.js diff --git a/lib/command/serve.js b/lib/command/serve.js new file mode 100644 index 0000000..e03f405 --- /dev/null +++ b/lib/command/serve.js @@ -0,0 +1,62 @@ +const ServeCommand = require('lws/lib/command/serve') +const path = require('path') + +/** + * @module local-web-server + */ + +class WsServe extends ServeCommand { + execute (options, argv) { + options = { + stack: [ + 'lws-log', + 'lws-cors', + 'lws-json', + 'lws-rewrite', + 'lws-body-parser', + 'lws-blacklist', + 'lws-conditional-get', + 'lws-mime', + 'lws-compress', + 'lws-mock-response', + 'lws-spa', + 'lws-static', + 'lws-index' + ], + moduleDir: path.resolve(__dirname, `../../node_modules`), + modulePrefix: 'lws-' + } + super.execute(options, argv) + } + + usage () { + const sections = super.usage() + sections.shift() + sections.shift() + sections.pop() + sections.unshift( + { + header: 'local-web-server', + content: 'A convenient local web server to support productive, full-stack Javascript development.' + }, + { + header: 'Synopsis', + content: [ + '$ ws ', + '$ ws [underline]{command} ' + ] + } + ) + sections.push({ + content: 'Project home: [underline]{https://github.com/lwsjs/local-web-server}' + }) + return sections + } + + showVersion () { + const pkg = require(path.resolve(__dirname, '..', 'package.json')) + console.log(pkg.version) + } +} + +module.exports = WsServe diff --git a/lib/local-web-server.js b/lib/local-web-server.js index cfe7c88..e69de29 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,77 +0,0 @@ -'use strict' -const Lws = require('lws') -const Serve = require('lws/lib/command/serve') -const path = require('path') - -/** - * @module local-web-server - */ - -class WsServe extends Serve { - execute (options, argv) { - options = { - stack: [ - 'lws-log', - 'lws-cors', - 'lws-json', - 'lws-rewrite', - 'lws-body-parser', - 'lws-blacklist', - 'lws-conditional-get', - 'lws-mime', - 'lws-compress', - 'lws-mock-response', - 'lws-spa', - 'lws-static', - 'lws-index' - ], - moduleDir: path.resolve(__dirname, `../node_modules`), - modulePrefix: 'lws-' - } - super.execute(options, argv) - } - - usage () { - const sections = super.usage() - sections.shift() - sections.shift() - sections.pop() - sections.unshift( - { - header: 'local-web-server', - content: 'A convenient local web server to support productive, full-stack Javascript development.' - }, - { - header: 'Synopsis', - content: [ - '$ ws ', - '$ ws [underline]{command} ' - ] - } - ) - sections.push({ - content: 'Project home: [underline]{https://github.com/lwsjs/local-web-server}' - }) - return sections - } - - showVersion () { - const pkg = require(path.resolve(__dirname, '..', 'package.json')) - console.log(pkg.version) - } -} - -/** - * @alias module:local-web-server - */ -class LocalWebServer extends Lws { - constructor (options) { - super (options) - /* override default serve command */ - this.commands.add(null, WsServe) - /* add feature-list command */ - this.commands.add('feature-list', require('./feature-list')) - } -} - -module.exports = LocalWebServer From 7dccc470df1dadc1317c5d31c77c56df7a0b3bf4 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 12 Jun 2017 00:18:32 +0100 Subject: [PATCH 077/136] main lib, fix tests --- lib/local-web-server.js | 29 ++++++++++++++++++++ package.json | 11 +++----- test/test.js | 5 ++-- yarn.lock | 72 +++++++++---------------------------------------- 4 files changed, 48 insertions(+), 69 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index e69de29..0062639 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -0,0 +1,29 @@ +const Lws = require('lws') +const path = require('path') + +class LocalWebServer extends Lws { + constructor (options) { + options = Object.assign({ + moduleDir: path.resolve(__dirname, `../../node_modules`), + modulePrefix: 'lws-', + stack: [ + 'lws-log', + 'lws-cors', + 'lws-json', + 'lws-rewrite', + 'lws-body-parser', + 'lws-blacklist', + 'lws-conditional-get', + 'lws-mime', + 'lws-compress', + 'lws-mock-response', + 'lws-spa', + 'lws-static', + 'lws-index' + ] + }, options) + super(options) + } +} + +module.exports = LocalWebServer diff --git a/package.json b/package.json index c0620e1..649217a 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,6 @@ "engines": { "node": ">=7.6" }, - "files": [ - "bin", - "lib", - "ssl" - ], "scripts": { "test": "test-runner test/*.js", "docs": "jsdoc2md -t jsdoc2md/api.hbs -p list lib/*.js > doc/api.md; echo", @@ -45,14 +40,14 @@ "lws-cors": "^0.2.0", "lws-index": "^0.2.0", "lws-json": "^0.2.0", - "lws-log": "^0.2.0", + "lws-log": "^0.2.1", "lws-mime": "^0.1.0", "lws-mock-response": "^0.1.0", - "lws-rewrite": "^0.2.0", + "lws-rewrite": "^0.2.1", "lws-spa": "^0.1.2", "lws-static": "^0.2.0" }, "devDependencies": { - "test-runner": "^0.3.0" + "test-runner": "^0.4.0" } } diff --git a/test/test.js b/test/test.js index 394c7d6..297a620 100644 --- a/test/test.js +++ b/test/test.js @@ -11,9 +11,10 @@ runner.test('basic', async function () { const localWebServer = new LocalWebServer({ port: port, directory: 'test/fixture', - 'log.format': 'none' + logFormat: 'none' }) - localWebServer.start() + localWebServer.attachView() + localWebServer.launch() const response = await request(`http://localhost:${port}/one.txt`) localWebServer.server.close() a.strictEqual(response.data.toString(), 'one\n') diff --git a/yarn.lock b/yarn.lock index de42545..ca70c88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -76,16 +76,7 @@ co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" -command-line-args@^3.0.1: - version "3.0.5" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-3.0.5.tgz#5bd4ad45e7983e5c1344918e40280ee2693c5ac0" - dependencies: - array-back "^1.0.4" - feature-detect-es6 "^1.3.1" - find-replace "^1.0.2" - typical "^2.6.0" - -command-line-args@^4.0.2: +command-line-args@^4.0.2, command-line-args@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-4.0.6.tgz#0ff87a1dd159890dcaeb2a005abdae71e55059fc" dependencies: @@ -93,27 +84,6 @@ command-line-args@^4.0.2: find-replace "^1.0.3" typical "^2.6.1" -command-line-tool@~0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/command-line-tool/-/command-line-tool-0.6.4.tgz#4c11e372f3e41254861c3fe6b538d3c7a5b144f3" - dependencies: - ansi-escape-sequences "^3.0.0" - array-back "^1.0.3" - command-line-args "^3.0.1" - command-line-usage "^3.0.3" - feature-detect-es6 "^1.3.1" - typical "^2.6.0" - -command-line-usage@^3.0.3: - version "3.0.8" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-3.0.8.tgz#b6a20978c1b383477f5c11a529428b880bfe0f4d" - dependencies: - ansi-escape-sequences "^3.0.0" - array-back "^1.0.3" - feature-detect-es6 "^1.3.1" - table-layout "^0.3.0" - typical "^2.6.0" - command-line-usage@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-4.0.0.tgz#816b32788b58f9feba44d1e6dac60fcaeb29b5ea" @@ -156,10 +126,6 @@ copy-to@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" -core-js@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" - core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -229,7 +195,7 @@ file-set@^1.1.1: array-back "^1.0.3" glob "^7.1.0" -find-replace@^1.0.2, find-replace@^1.0.3: +find-replace@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0" dependencies: @@ -521,9 +487,9 @@ lws-json@^0.2.0: dependencies: koa-json "^2.0.2" -lws-log@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/lws-log/-/lws-log-0.2.0.tgz#b744105c8983b4d0bbe89a0677970684007137f3" +lws-log@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/lws-log/-/lws-log-0.2.1.tgz#c6ec3b850597c985abb6f8c3091aded5fe7616e2" dependencies: koa-morgan "^1.0.1" stream-log-stats "^2.0.2" @@ -539,7 +505,7 @@ lws-mock-response@^0.1.0: array-back "^1.0.4" koa-mock-response "0.0.2" -lws-rewrite@^0.2.0: +lws-rewrite@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/lws-rewrite/-/lws-rewrite-0.2.1.tgz#5b15cb466237d72f2a9d24efcb6040d3f0729621" dependencies: @@ -782,17 +748,6 @@ string_decoder@~1.0.0: dependencies: safe-buffer "~5.0.1" -table-layout@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.3.0.tgz#6ee20dc483db371b3e5c87f704ed2f7c799d2c9a" - dependencies: - array-back "^1.0.3" - core-js "^2.4.1" - deep-extend "~0.4.1" - feature-detect-es6 "^1.3.1" - typical "^2.6.0" - wordwrapjs "^2.0.0-0" - table-layout@^0.4.0, table-layout@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.0.tgz#c70ff0455d9add63b91f7c15a77926295c0e0e7d" @@ -803,15 +758,14 @@ table-layout@^0.4.0, table-layout@~0.4.0: typical "^2.6.0" wordwrapjs "^2.0.0" -test-runner@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/test-runner/-/test-runner-0.3.0.tgz#d1c98a11d15a035bda6ef2342a104e70251a8d58" +test-runner@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/test-runner/-/test-runner-0.4.0.tgz#41426f9b3dfff4bcda2cb23f3b7df73d67d6b8a2" dependencies: ansi-escape-sequences "^3.0.0" - array-back "^1.0.3" - command-line-tool "~0.6.4" - core-js "^2.4.1" - feature-detect-es6 "^1.3.1" + array-back "^1.0.4" + command-line-args "^4.0.6" + command-line-usage "^4.0.0" file-set "^1.1.1" reduce-flatten "^1.0.1" @@ -865,7 +819,7 @@ walk-back@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-3.0.0.tgz#2358787a35da91032dad5e92f80b12370d8795c5" -wordwrapjs@^2.0.0, wordwrapjs@^2.0.0-0: +wordwrapjs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-2.0.0.tgz#ab55f695e6118da93858fdd70c053d1c5e01ac20" dependencies: From b17914fced2782f54a0d8d0c9095aa2fb1947178 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 15 Jun 2017 17:40:35 +0100 Subject: [PATCH 078/136] passing test --- test/test.js | 1 - yarn.lock | 833 ----------------------------------------------------------- 2 files changed, 834 deletions(-) delete mode 100644 yarn.lock diff --git a/test/test.js b/test/test.js index 297a620..e529edd 100644 --- a/test/test.js +++ b/test/test.js @@ -13,7 +13,6 @@ runner.test('basic', async function () { directory: 'test/fixture', logFormat: 'none' }) - localWebServer.attachView() localWebServer.launch() const response = await request(`http://localhost:${port}/one.txt`) localWebServer.server.close() diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index ca70c88..0000000 --- a/yarn.lock +++ /dev/null @@ -1,833 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -JSONStream@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - -accepts@^1.2.2, accepts@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" - dependencies: - mime-types "~2.1.11" - negotiator "0.6.1" - -ansi-escape-sequences@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-3.0.0.tgz#1c18394b6af9b76ff9a63509fa497669fd2ce53e" - dependencies: - array-back "^1.0.3" - -any-promise@^1.0.0, any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - -array-back@^1.0.3, array-back@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b" - dependencies: - typical "^2.6.0" - -balanced-match@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - -basic-auth@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" - -batch@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464" - -brace-expansion@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - -byte-size@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-3.0.0.tgz#406f9e2366aa5dabf63672eb291d7bb09495da75" - -bytes@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" - -bytes@^2.3.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" - -co-body@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/co-body/-/co-body-5.1.1.tgz#d97781d1e3344ba4a820fd1806bddf8341505236" - dependencies: - inflation "^2.0.0" - qs "^6.4.0" - raw-body "^2.2.0" - type-is "^1.6.14" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -command-line-args@^4.0.2, command-line-args@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-4.0.6.tgz#0ff87a1dd159890dcaeb2a005abdae71e55059fc" - dependencies: - array-back "^1.0.4" - find-replace "^1.0.3" - typical "^2.6.1" - -command-line-usage@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-4.0.0.tgz#816b32788b58f9feba44d1e6dac60fcaeb29b5ea" - dependencies: - ansi-escape-sequences "^3.0.0" - array-back "^1.0.4" - table-layout "^0.4.0" - typical "^2.6.0" - -common-log-format@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/common-log-format/-/common-log-format-0.1.3.tgz#6009dadc4ec467371af28a8dbd3e4ce02b166782" - -compressible@^2.0.0: - version "2.0.10" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" - dependencies: - mime-db ">= 1.27.0 < 2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -content-disposition@~0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - -content-type@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" - -cookies@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.0.tgz#0bc961d910c35254980fc7c9eff5da12011bbf00" - dependencies: - depd "~1.1.0" - keygrip "~1.0.1" - -copy-to@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -debug@*, debug@2.6.8, debug@^2.6.0, debug@^2.6.3: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" - dependencies: - ms "2.0.0" - -debug@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" - dependencies: - ms "0.7.2" - -deep-equal@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" - -deep-extend@~0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" - -defer-promise@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/defer-promise/-/defer-promise-1.0.1.tgz#1ca6ffeddbcef1715dd7aae25c7616f9ae22932f" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - -depd@1.1.0, depd@^1.1.0, depd@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" - -destroy@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - -error-inject@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" - -escape-html@~1.0.1, escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - -etag@^1.3.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" - -feature-detect-es6@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/feature-detect-es6/-/feature-detect-es6-1.3.1.tgz#f888736af9cb0c91f55663bfa4762eb96ee7047f" - dependencies: - array-back "^1.0.3" - -file-set@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/file-set/-/file-set-1.1.1.tgz#d3ec70c080ec8f18f204ba1de106780c9056926b" - dependencies: - array-back "^1.0.3" - glob "^7.1.0" - -find-replace@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0" - dependencies: - array-back "^1.0.4" - test-value "^2.1.0" - -fresh@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - -glob@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -http-assert@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a" - dependencies: - deep-equal "~1.0.1" - http-errors "~1.6.1" - -http-errors@^1.2.8, http-errors@^1.6.1, http-errors@~1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" - dependencies: - depd "1.1.0" - inherits "2.0.3" - setprototypeof "1.0.3" - statuses ">= 1.3.1 < 2" - -http-errors@~1.5.0, http-errors@~1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" - dependencies: - inherits "2.0.3" - setprototypeof "1.0.2" - statuses ">= 1.3.1 < 2" - -iconv-lite@0.4.15: - version "0.4.15" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" - -inflation@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@~2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -is-generator-function@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.6.tgz#9e71653cd15fff341c79c4151460a131d31e9fc4" - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -json-stringify-safe@5: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - -kcors@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/kcors/-/kcors-2.2.1.tgz#7160a94f2eae633436d2cef8eadd0ce232386779" - -keygrip@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.1.tgz#b02fa4816eef21a8c4b35ca9e52921ffc89a30e9" - -koa-bodyparser@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.2.0.tgz#bce6e08bc65f8709b6d1faa9411c7f0d8938aa54" - dependencies: - co-body "^5.1.0" - copy-to "^2.0.1" - -koa-compose@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" - dependencies: - any-promise "^1.1.0" - -koa-compress@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-2.0.0.tgz#7b7eb2921b847746b5e122ba9f5cd8a671e8ea3a" - dependencies: - bytes "^2.3.0" - compressible "^2.0.0" - koa-is-json "^1.0.0" - statuses "^1.0.0" - -koa-conditional-get@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/koa-conditional-get/-/koa-conditional-get-2.0.0.tgz#a43f3723c1d014b730a34ece8adf30b93c8233f2" - -koa-convert@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" - dependencies: - co "^4.6.0" - koa-compose "^3.0.0" - -koa-etag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-3.0.0.tgz#9ef7382ddd5a82ab0deb153415c915836f771d3f" - dependencies: - etag "^1.3.0" - mz "^2.1.0" - -koa-is-json@1, koa-is-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" - -koa-json@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/koa-json/-/koa-json-2.0.2.tgz#36af14e6ea1f5d646d7c44a285701c6f85a4fde4" - dependencies: - koa-is-json "1" - streaming-json-stringify "3" - -koa-mock-response@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/koa-mock-response/-/koa-mock-response-0.0.2.tgz#fc29e9a07bd0bdaeb5a79a1007f371423460b10d" - dependencies: - array-back "^1.0.3" - path-to-regexp "^1.5.3" - test-value "^2.0.0" - typical "^2.4.2" - -koa-morgan@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/koa-morgan/-/koa-morgan-1.0.1.tgz#08052e0ce0d839d3c43178b90a5bb3424bef1f99" - dependencies: - morgan "^1.6.1" - -koa-rewrite@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/koa-rewrite/-/koa-rewrite-2.1.0.tgz#812b35f48a860031bc862f88135e5c28318b7aa8" - dependencies: - debug "*" - path-to-regexp "0.0.2" - -koa-route@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/koa-route/-/koa-route-3.2.0.tgz#76298b99a6bcfa9e38cab6fe5c79a8733e758bce" - dependencies: - debug "*" - methods "~1.1.0" - path-to-regexp "^1.2.0" - -koa-send@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-3.3.0.tgz#5a4ae245564680c6ecf6079e9275fa5173a861dc" - dependencies: - co "^4.6.0" - debug "^2.6.0" - mz "^2.3.1" - resolve-path "^1.3.1" - -koa-send@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-4.1.0.tgz#07d5a4eaab212679fe99916aae6b1109c08c2361" - dependencies: - debug "^2.6.3" - http-errors "^1.6.1" - mz "^2.6.0" - resolve-path "^1.3.3" - -koa-static@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-3.0.0.tgz#40442233d2c0b35c225e450199c10bf8a539e416" - dependencies: - debug "*" - koa-send "^3.2.0" - -koa@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.2.0.tgz#b055933187849d540ad8b9f731baaa4be97c652d" - dependencies: - accepts "^1.2.2" - content-disposition "~0.5.0" - content-type "^1.0.0" - cookies "~0.7.0" - debug "*" - delegates "^1.0.0" - depd "^1.1.0" - destroy "^1.0.3" - error-inject "~1.0.0" - escape-html "~1.0.1" - fresh "^0.5.0" - http-assert "^1.1.0" - http-errors "^1.2.8" - is-generator-function "^1.0.3" - koa-compose "^3.0.0" - koa-convert "^1.2.0" - koa-is-json "^1.0.0" - mime-types "^2.0.7" - on-finished "^2.1.0" - only "0.0.2" - parseurl "^1.3.0" - statuses "^1.2.0" - type-is "^1.5.5" - vary "^1.0.0" - -lodash.assignwith@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb" - -lodash.padend@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - -lws-blacklist@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lws-blacklist/-/lws-blacklist-0.1.0.tgz#2e6497444c00fa646248db7389867763d5ab5a3a" - dependencies: - array-back "^1.0.4" - path-to-regexp "^1.7.0" - -lws-body-parser@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lws-body-parser/-/lws-body-parser-0.1.0.tgz#19efc26a60824f50b9f61fbc0a7e827c8ba7bc65" - dependencies: - koa-bodyparser "^4.1.0" - -lws-compress@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lws-compress/-/lws-compress-0.1.0.tgz#c2e68d7dfa5345e6f4d19119535c69685e809094" - dependencies: - koa-compress "^2.0.0" - -lws-conditional-get@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lws-conditional-get/-/lws-conditional-get-0.1.0.tgz#5025f655fed6e285c0efe8103e43e9e751324a92" - dependencies: - koa-conditional-get "^2.0.0" - koa-etag "^3.0.0" - -lws-cors@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/lws-cors/-/lws-cors-0.2.0.tgz#de1c8863b1366d8a680fc4ad834b5b0709007cac" - dependencies: - kcors "^2.2.1" - -lws-index@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/lws-index/-/lws-index-0.2.0.tgz#ff6b109efa748d648e0c8a65956daa7fdb576e3d" - dependencies: - serve-index-75lb "^1.8.1" - -lws-json@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/lws-json/-/lws-json-0.2.0.tgz#8386206506ae0a4cfe6fc6d74b75b8f25ac7c17f" - dependencies: - koa-json "^2.0.2" - -lws-log@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/lws-log/-/lws-log-0.2.1.tgz#c6ec3b850597c985abb6f8c3091aded5fe7616e2" - dependencies: - koa-morgan "^1.0.1" - stream-log-stats "^2.0.2" - -lws-mime@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lws-mime/-/lws-mime-0.1.0.tgz#9b6c29cd26640d61f0da810fbdf2224e28fb194f" - -lws-mock-response@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/lws-mock-response/-/lws-mock-response-0.1.0.tgz#2ae72aef1c9e90ec4bab47770fb81346d7d5e2f5" - dependencies: - array-back "^1.0.4" - koa-mock-response "0.0.2" - -lws-rewrite@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/lws-rewrite/-/lws-rewrite-0.2.1.tgz#5b15cb466237d72f2a9d24efcb6040d3f0729621" - dependencies: - array-back "^1.0.4" - koa-rewrite "^2.1.0" - koa-route "^3.2.0" - path-to-regexp "^1.7.0" - req-then "~0.6.1" - typical "^2.6.1" - -lws-spa@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/lws-spa/-/lws-spa-0.1.2.tgz#97fda9d5e1b5bceca1a4558175655190b115e8fe" - dependencies: - koa-route "^3.2.0" - koa-send "^4.1.0" - -lws-static@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/lws-static/-/lws-static-0.2.0.tgz#74df9be31ece6534894356fa7d884b53cf1075d4" - dependencies: - koa-static "^3.0.0" - -lws@^1.0.0-pre.4: - version "1.0.0-pre.4" - resolved "https://registry.yarnpkg.com/lws/-/lws-1.0.0-pre.4.tgz#d051a7919d1ff776cc39c1955f4ec48d4d0e2e7c" - dependencies: - ansi-escape-sequences "^3.0.0" - array-back "^1.0.4" - command-line-args "^4.0.2" - command-line-usage "^4.0.0" - koa "^2.2.0" - lodash.assignwith "^4.2.0" - reduce-flatten "^1.0.1" - typical "^2.6.0" - walk-back "^3.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - -methods@~1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - -"mime-db@>= 1.27.0 < 2": - version "1.28.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.28.0.tgz#fedd349be06d2865b7fc57d837c6de4f17d7ac3c" - -mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" - -mime-types@^2.0.7, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.15: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" - dependencies: - mime-db "~1.27.0" - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - dependencies: - brace-expansion "^1.1.7" - -morgan@^1.6.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.8.2.tgz#784ac7734e4a453a9c6e6e8680a9329275c8b687" - dependencies: - basic-auth "~1.1.0" - debug "2.6.8" - depd "~1.1.0" - on-finished "~2.3.0" - on-headers "~1.0.1" - -ms@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -mz@^2.1.0, mz@^2.3.1, mz@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.6.0.tgz#c8b8521d958df0a4f2768025db69c719ee4ef1ce" - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" - -object-assign@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - -on-finished@^2.1.0, on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - dependencies: - wrappy "1" - -only@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" - -parseurl@^1.3.0, parseurl@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" - -path-is-absolute@1.0.1, path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-to-regexp@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.0.2.tgz#489feb060b314443a5494ab1da2efed2040ab24c" - -path-to-regexp@^1.2.0, path-to-regexp@^1.5.3, path-to-regexp@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" - dependencies: - isarray "0.0.1" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -qs@^6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - -raw-body@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" - dependencies: - bytes "2.4.0" - iconv-lite "0.4.15" - unpipe "1.0.0" - -readable-stream@2: - version "2.2.11" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - safe-buffer "~5.0.1" - string_decoder "~1.0.0" - util-deprecate "~1.0.1" - -reduce-flatten@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327" - -req-then@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/req-then/-/req-then-0.6.1.tgz#e85af6b939ffcc4b4f145b7e51cae73e8f8052c9" - dependencies: - array-back "^1.0.4" - defer-promise "^1.0.1" - lodash.pick "^4.4.0" - typical "^2.6.0" - -resolve-path@^1.3.1, resolve-path@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.3.3.tgz#4d83aba6468c2b8e632a575e3f52b0fa0dbe1a5c" - dependencies: - http-errors "~1.5.0" - path-is-absolute "1.0.1" - -safe-buffer@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" - -serve-index-75lb@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/serve-index-75lb/-/serve-index-75lb-1.8.1.tgz#5929f756db1e0796ef7c785e981dd39e26b0efc2" - dependencies: - accepts "~1.3.3" - batch "0.5.3" - debug "2.3.3" - escape-html "~1.0.3" - http-errors "~1.5.1" - mime-types "~2.1.13" - parseurl "~1.3.1" - -setprototypeof@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" - -setprototypeof@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" - -"statuses@>= 1.3.1 < 2", statuses@^1.0.0, statuses@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" - -stream-log-stats@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-log-stats/-/stream-log-stats-2.0.2.tgz#a59a4af9e224fa124457dd9b8ba89c757dc3dab0" - dependencies: - JSONStream "^1.3.1" - ansi-escape-sequences "^3.0.0" - byte-size "^3.0.0" - common-log-format "~0.1.3" - lodash.throttle "^4.1.1" - stream-via "^1.0.3" - table-layout "~0.4.0" - -stream-via@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/stream-via/-/stream-via-1.0.4.tgz#8dccbb0ac909328eb8bc8e2a4bd3934afdaf606c" - -streaming-json-stringify@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz#80200437a993cc39c4fe00263b7b3b903ac87af5" - dependencies: - json-stringify-safe "5" - readable-stream "2" - -string_decoder@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.2.tgz#b29e1f4e1125fa97a10382b8a533737b7491e179" - dependencies: - safe-buffer "~5.0.1" - -table-layout@^0.4.0, table-layout@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.0.tgz#c70ff0455d9add63b91f7c15a77926295c0e0e7d" - dependencies: - array-back "^1.0.4" - deep-extend "~0.4.1" - lodash.padend "^4.6.1" - typical "^2.6.0" - wordwrapjs "^2.0.0" - -test-runner@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/test-runner/-/test-runner-0.4.0.tgz#41426f9b3dfff4bcda2cb23f3b7df73d67d6b8a2" - dependencies: - ansi-escape-sequences "^3.0.0" - array-back "^1.0.4" - command-line-args "^4.0.6" - command-line-usage "^4.0.0" - file-set "^1.1.1" - reduce-flatten "^1.0.1" - -test-value@^2.0.0, test-value@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291" - dependencies: - array-back "^1.0.3" - typical "^2.6.0" - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.0" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" - dependencies: - any-promise "^1.0.0" - -"through@>=2.2.7 <3": - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - -type-is@^1.5.5, type-is@^1.6.14: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.15" - -typical@^2.4.2, typical@^2.6.0, typical@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" - -unpipe@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -vary@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" - -walk-back@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-3.0.0.tgz#2358787a35da91032dad5e92f80b12370d8795c5" - -wordwrapjs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-2.0.0.tgz#ab55f695e6118da93858fdd70c053d1c5e01ac20" - dependencies: - array-back "^1.0.3" - feature-detect-es6 "^1.3.1" - reduce-flatten "^1.0.1" - typical "^2.6.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" From 9b1900481dec1b54b872a4f4ebc0acec66ee1284 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 15 Jun 2017 17:47:12 +0100 Subject: [PATCH 079/136] deps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 649217a..5bd97b8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre.4", + "lws": "^1.0.0-pre.5", "lws-blacklist": "^0.1.0", "lws-body-parser": "^0.1.0", "lws-compress": "^0.1.0", From f1a655a3487d3e1e7b890f102747d9ec196ca1c5 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 16 Jun 2017 23:49:44 +0100 Subject: [PATCH 080/136] rename feature-list to middleware-list.. fix --version --- lib/cli-app.js | 4 ++-- lib/command/{feature-list.js => middleware-list.js} | 6 +++--- lib/command/serve.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/command/{feature-list.js => middleware-list.js} (79%) diff --git a/lib/cli-app.js b/lib/cli-app.js index 941003c..da2b862 100644 --- a/lib/cli-app.js +++ b/lib/cli-app.js @@ -9,8 +9,8 @@ class WsCliApp extends LwsCliApp { super (options) /* override default serve command */ this.commands.add(null, require('./command/serve')) - /* add feature-list command */ - this.commands.add('feature-list', require('./command/feature-list')) + /* add middleware-list command */ + this.commands.add('middleware-list', require('./command/middleware-list')) } } diff --git a/lib/command/feature-list.js b/lib/command/middleware-list.js similarity index 79% rename from lib/command/feature-list.js rename to lib/command/middleware-list.js index 27a4e31..caf2f65 100644 --- a/lib/command/feature-list.js +++ b/lib/command/middleware-list.js @@ -1,6 +1,6 @@ -class FeatureList { +class MiddlewareList { description () { - return 'Print installed features' + return 'Print available middleware' } execute (options) { const list = [ @@ -22,4 +22,4 @@ class FeatureList { } } -module.exports = FeatureList +module.exports = MiddlewareList diff --git a/lib/command/serve.js b/lib/command/serve.js index e03f405..bde4dd4 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -54,7 +54,7 @@ class WsServe extends ServeCommand { } showVersion () { - const pkg = require(path.resolve(__dirname, '..', 'package.json')) + const pkg = require(path.resolve(__dirname, '..', '..', 'package.json')) console.log(pkg.version) } } From 582b97c04717c1b28d57f46d72bac7903144e3ee Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 17 Jun 2017 00:14:16 +0100 Subject: [PATCH 081/136] deps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5bd97b8..07ae678 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre.5", + "lws": "^1.0.0-pre.6", "lws-blacklist": "^0.1.0", "lws-body-parser": "^0.1.0", "lws-compress": "^0.1.0", From 05154dcd5fe163a48f2a8daecf7a5fc4fe986b8f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 17 Jun 2017 00:14:31 +0100 Subject: [PATCH 082/136] 2.0.0-pre.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07ae678..863bab6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre.1", + "version": "2.0.0-pre.2", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From 72a357c66d8f30f0bb1c7b94d1215a87b8f403bc Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 17 Jun 2017 00:41:38 +0100 Subject: [PATCH 083/136] readme --- README.md | 101 +++----------------------------------------------------------- 1 file changed, 4 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index b346dcb..7b7b392 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![view on npm](http://img.shields.io/npm/v/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) -[![npm module downloads](http://img.shields.io/npm/dt/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) +[![npm (tag)](https://img.shields.io/npm/v/local-web-server/next.svg)](https://www.npmjs.org/package/local-web-server) +[![npm module downloads](https://img.shields.io/npm/dt/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) [![Build Status](https://travis-ci.org/lwsjs/local-web-server.svg?branch=next)](https://travis-ci.org/lwsjs/local-web-server) [![Dependency Status](https://david-dm.org/lwsjs/local-web-server/next.svg)](https://david-dm.org/lwsjs/local-web-server/next) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) @@ -9,105 +9,12 @@ # local-web-server -The development web server for productive front-end and full-stack Javascript engineers. Built on [lws](https://github.com/lwsjs/lws). - -**Features** - -- Fast and lightweight. -- Use the built-in features, a subset of the built-ins or your own feature stack -- Configurable with sensible defaults. - - Configure by constructor option, command-line option, stored config or all three. -- HTTP or HTTPS ([HTTP2](https://github.com/nodejs/http2) will be added once ready) -- URL rewriting - - Local rewrites for quick experimentation (e.g. from `/img/logo.svg` to `/img/new-logo.svg`) - - Rewrite to remote resources (e.g. from `/api/*` to `https://example-api.pl/api/$1`). *Note: ignores remote server's CORS policy, which during development is typically what you want*. -- Optimisal caching by default. - - Efficient, predictable, entity-tag-powered conditional request handling (no need to 'Disable Cache' in DevTools, slowing page-load down) -- Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/local-web-server/blob/master/doc/visualisation.md) -- Configurable CORS rules. All origins allowed by default. - -**Links to demoes and how-tos** - -Things you can build: - -- Simple static website -- Single Page Application - - Works well with React, Angular or vanilla JS. -- Real or mock web services - - e.g. a RESTful API or microservice - - Mocks are defined with config (static), or code (dynamic). -- Websocket server - -## Synopsis - -local-web-server is a command-line tool. To serve the current directory, run `ws`. - -
    $ ws --help
    -
    -local-web-server
    -
    -  A convenient local web server to support productive, full-stack Javascript
    -  development.
    -
    -Synopsis
    -
    -  $ ws [--verbose] [--config-file file] [<server options>] [<middleware options>]
    -  $ ws --config
    -  $ ws --help
    -  $ ws --version
    -
    -General
    -
    -  -h, --help               Print these usage instructions.
    -  --config                 Print the active config.
    -  -c, --config-file file   Config filename to use, defaults to "lws.config.js".
    -  -v, --verbose            Verbose output.
    -  --version                Print the version number.
    -
    -Server
    -
    -  -p, --port number     Web server port.
    -  --hostname string     The hostname (or IP address) to listen on. Defaults to 0.0.0.0.
    -  --stack feature ...   Feature stack.
    -  --key file            SSL key. Supply along with --cert to launch a https server.
    -  --cert file           SSL cert. Supply along with --key to launch a https server.
    -  --https               Enable HTTPS using a built-in key and cert, registered to the domain
    -                        127.0.0.1.
    -
    -Middleware
    -
    -  -f, --log.format string        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', 'stats', or 'logstalgia') or supply a custom format (e.g.
    -                                 ':method -> :url').
    -  --cors.origin                  Access-Control-Allow-Origin value. Default is request Origin header.
    -  --cors.allow-methods           Access-Control-Allow-Methods value. Default is
    -                                 "GET,HEAD,PUT,POST,DELETE,PATCH"
    -  -r, --rewrite expression ...   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'.
    -  -b, --forbid path ...          A list of forbidden routes.
    -  -n, --no-cache                 Disable etag-based caching - forces loading from disk each request.
    -  -z, --compress                 Serve gzip-compressed resources, where applicable.
    -  --compress.threshold number    Minimum response size in bytes to apply compression. Defaults to 1024 bytes.
    -  --spa file                     Path to a Single Page App, e.g. app.html.
    -  --spa.asset-test RegExp        A regular expression to identify an asset file. Defaults to "\.".
    -  -d, --directory path           Root directory, defaults to the current directory.
    -  --static.maxage number         Browser cache max-age in milliseconds.
    -  --static.defer                 If true, serves after `yield next`, allowing any downstream middleware to
    -                                 respond first.
    -  --static.index path            Default file name, defaults to `index.html`.
    -  --index.root path              Index root directory, defaults to --directory or the current directory.
    -  --index.hidden                 Show hidden files.
    -  --index.view name              Display mode, either `tiles` or `details`. Defaults to tiles.
    -
    -  Project home: https://github.com/lwsjs/local-web-server
    -
    +*Documentation coming soon.* ## Install ```sh -$ npm install -g local-web-server +$ npm install -g local-web-server@next ``` * * * From 05fa42ecbe63d76db6b4f769b089e0b198f19e99 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 18 Jun 2017 22:58:21 +0100 Subject: [PATCH 084/136] deps --- example/built-in/mock/mocks/users2.js | 37 +++++++++++++++++++++++++++++++++++ package.json | 16 +++++++-------- 2 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 example/built-in/mock/mocks/users2.js diff --git a/example/built-in/mock/mocks/users2.js b/example/built-in/mock/mocks/users2.js new file mode 100644 index 0000000..491aa45 --- /dev/null +++ b/example/built-in/mock/mocks/users2.js @@ -0,0 +1,37 @@ +const users = require('./users.json') + +/* responses for /users */ +const userResponses = [ + { + route: '/users', + responses: [ + /* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ + { request: { method: 'PUT' }, response: { status: 400 } }, + { request: { method: 'DELETE' }, response: { status: 400 } }, + { + /* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ + request: { method: 'GET' }, + response: function (ctx) { + ctx.body = users.filter(user => { + const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) + const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) + return meetsMinAge && requiredNationality + }) + } + }, + { + /* for POST requests, create a new user and return the path to the new resource */ + request: { method: 'POST' }, + response: function (ctx) { + const newUser = ctx.request.body + users.push(newUser) + newUser.id = users.length + ctx.status = 201 + ctx.response.set('Location', `/users/${newUser.id}`) + } + } + ] + } +] + +module.exports = userResponses diff --git a/package.json b/package.json index 863bab6..25a3cc8 100644 --- a/package.json +++ b/package.json @@ -33,18 +33,18 @@ "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { "lws": "^1.0.0-pre.6", - "lws-blacklist": "^0.1.0", - "lws-body-parser": "^0.1.0", - "lws-compress": "^0.1.0", - "lws-conditional-get": "^0.1.0", + "lws-blacklist": "^0.2.0", + "lws-body-parser": "^0.2.0", + "lws-compress": "^0.2.0", + "lws-conditional-get": "^0.2.0", "lws-cors": "^0.2.0", "lws-index": "^0.2.0", "lws-json": "^0.2.0", "lws-log": "^0.2.1", - "lws-mime": "^0.1.0", - "lws-mock-response": "^0.1.0", - "lws-rewrite": "^0.2.1", - "lws-spa": "^0.1.2", + "lws-mime": "^0.2.0", + "lws-mock-response": "^0.2.0", + "lws-rewrite": "^0.2.2", + "lws-spa": "^0.2.0", "lws-static": "^0.2.0" }, "devDependencies": { From 144163a7a016c07fbf2408451fae826bb85383b9 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 18 Jun 2017 22:58:33 +0100 Subject: [PATCH 085/136] 2.0.0-pre.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25a3cc8..e5b6db2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre.2", + "version": "2.0.0-pre.3", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From 2fc4292880b7083a191fe0916f4b0ba8584e08b5 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 21 Jun 2017 01:07:11 +0100 Subject: [PATCH 086/136] deps --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e5b6db2..37702bf 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre.6", + "lws": "^1.0.0-pre.7", "lws-blacklist": "^0.2.0", "lws-body-parser": "^0.2.0", "lws-compress": "^0.2.0", @@ -40,10 +40,10 @@ "lws-cors": "^0.2.0", "lws-index": "^0.2.0", "lws-json": "^0.2.0", - "lws-log": "^0.2.1", + "lws-log": "^0.2.2", "lws-mime": "^0.2.0", "lws-mock-response": "^0.2.0", - "lws-rewrite": "^0.2.2", + "lws-rewrite": "^0.2.3", "lws-spa": "^0.2.0", "lws-static": "^0.2.0" }, From 222d4498b5e30ace97a9a9b2c553d1d48bed5ab3 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 21 Jun 2017 01:07:17 +0100 Subject: [PATCH 087/136] 2.0.0-pre.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 37702bf..6c1f7df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre.3", + "version": "2.0.0-pre.4", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From b8883157831769e3eb3080128058dbd4bafe77fb Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 21 Jun 2017 22:19:05 +0100 Subject: [PATCH 088/136] move docs to wiki and examples to separate project --- .travis.yml | 1 + doc/api.md | 72 ------ doc/blacklist.md | 9 - doc/distribute.md | 32 --- doc/https.md | 59 ----- doc/img/logstagia.gif | Bin 583963 -> 0 bytes doc/logging.md | 69 ------ doc/mime-types.md | 12 - doc/mock-response.md | 241 --------------------- doc/rewrite.md | 37 ---- doc/spa.md | 12 - doc/stored-config.md | 28 --- doc/visualisation.md | 56 ----- example/built-in/forbid/.local-web-server.json | 5 - example/built-in/forbid/admin/blocked.html | 1 - example/built-in/forbid/allowed.html | 1 - example/built-in/forbid/index.html | 5 - example/built-in/forbid/something.php | 1 - .../built-in/mime-override/.local-web-server.json | 5 - example/built-in/mime-override/something.php | 1 - example/built-in/mock-async/.local-web-server.json | 8 - example/built-in/mock-async/mocks/delayed.js | 11 - example/built-in/mock/.local-web-server.json | 58 ----- example/built-in/mock/mocks/five.js | 6 - example/built-in/mock/mocks/stream-self.js | 8 - example/built-in/mock/mocks/user.js | 41 ---- example/built-in/mock/mocks/users.js | 32 --- example/built-in/mock/mocks/users.json | 5 - example/built-in/mock/mocks/users2.js | 37 ---- example/built-in/rewrite/.local-web-server.json | 8 - example/built-in/rewrite/build/styles/style.css | 4 - example/built-in/rewrite/index.html | 24 -- example/built-in/rewrite/lws.config.js | 8 - example/built-in/simple/css/style.css | 7 - example/built-in/simple/index.html | 10 - example/built-in/simple/package.json | 7 - example/built-in/spa/.local-web-server.json | 4 - example/built-in/spa/css/style.css | 5 - example/built-in/spa/image.jpg | Bin 2313 -> 0 bytes example/built-in/spa/index.html | 14 -- 40 files changed, 1 insertion(+), 943 deletions(-) delete mode 100644 doc/api.md delete mode 100644 doc/blacklist.md delete mode 100644 doc/distribute.md delete mode 100644 doc/https.md delete mode 100644 doc/img/logstagia.gif delete mode 100644 doc/logging.md delete mode 100644 doc/mime-types.md delete mode 100644 doc/mock-response.md delete mode 100644 doc/rewrite.md delete mode 100644 doc/spa.md delete mode 100644 doc/stored-config.md delete mode 100644 doc/visualisation.md delete mode 100644 example/built-in/forbid/.local-web-server.json delete mode 100644 example/built-in/forbid/admin/blocked.html delete mode 100644 example/built-in/forbid/allowed.html delete mode 100644 example/built-in/forbid/index.html delete mode 100644 example/built-in/forbid/something.php delete mode 100644 example/built-in/mime-override/.local-web-server.json delete mode 100644 example/built-in/mime-override/something.php delete mode 100644 example/built-in/mock-async/.local-web-server.json delete mode 100644 example/built-in/mock-async/mocks/delayed.js delete mode 100644 example/built-in/mock/.local-web-server.json delete mode 100644 example/built-in/mock/mocks/five.js delete mode 100644 example/built-in/mock/mocks/stream-self.js delete mode 100644 example/built-in/mock/mocks/user.js delete mode 100644 example/built-in/mock/mocks/users.js delete mode 100644 example/built-in/mock/mocks/users.json delete mode 100644 example/built-in/mock/mocks/users2.js delete mode 100644 example/built-in/rewrite/.local-web-server.json delete mode 100644 example/built-in/rewrite/build/styles/style.css delete mode 100644 example/built-in/rewrite/index.html delete mode 100644 example/built-in/rewrite/lws.config.js delete mode 100644 example/built-in/simple/css/style.css delete mode 100644 example/built-in/simple/index.html delete mode 100644 example/built-in/simple/package.json delete mode 100644 example/built-in/spa/.local-web-server.json delete mode 100644 example/built-in/spa/css/style.css delete mode 100644 example/built-in/spa/image.jpg delete mode 100644 example/built-in/spa/index.html diff --git a/.travis.yml b/.travis.yml index fb0b4ac..343e041 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: node_js node_js: - 7 + - 8 diff --git a/doc/api.md b/doc/api.md deleted file mode 100644 index 4b4ccf0..0000000 --- a/doc/api.md +++ /dev/null @@ -1,72 +0,0 @@ -## API Reference - - -* [local-web-server](#module_local-web-server) - * [LocalWebServer](#exp_module_local-web-server--LocalWebServer) ⇐ module:middleware-stack ⏏ - * [new LocalWebServer([options])](#new_module_local-web-server--LocalWebServer_new) - * _instance_ - * [.features](#module_local-web-server--LocalWebServer.LocalWebServer+features) : Array.<Feature> - * [.options](#module_local-web-server--LocalWebServer.LocalWebServer+options) : object - * [.view](#module_local-web-server--LocalWebServer.LocalWebServer+view) : View - * [.server](#module_local-web-server--LocalWebServer.LocalWebServer+server) : Server - * [.getApplication()](#module_local-web-server--LocalWebServer+getApplication) ⇒ function - * [.getServer()](#module_local-web-server--LocalWebServer+getServer) ⇒ Server - * _inner_ - * [~loadStack()](#module_local-web-server--LocalWebServer..loadStack) ⇒ object - - - -### LocalWebServer ⇐ module:middleware-stack ⏏ -**Kind**: Exported class -**Extends:** module:middleware-stack - - -#### new LocalWebServer([options]) -**Params** - -- [options] object - Server options - - .port} number - Port - - .stack} Array.<string> | Array.<Features> - Port - - - -#### localWebServer.features : Array.<Feature> -Loaded feature modules - -**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - - -#### localWebServer.options : object -Config - -**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - - -#### localWebServer.view : View -Current view. - -**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - - -#### localWebServer.server : Server -Node.js server - -**Kind**: instance property of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - - -#### localWebServer.getApplication() ⇒ function -Returns a middleware application suitable for passing to `http.createServer`. The application is a function with three args (req, res and next) which can be created by express, Koa or hand-rolled. - -**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - - -#### localWebServer.getServer() ⇒ Server -Returns a listening server which processes requests using the middleware supplied. - -**Kind**: instance method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) - - -#### LocalWebServer~loadStack() ⇒ object -Loads a module by either path or name. - -**Kind**: inner method of [LocalWebServer](#exp_module_local-web-server--LocalWebServer) diff --git a/doc/blacklist.md b/doc/blacklist.md deleted file mode 100644 index 23a4d52..0000000 --- a/doc/blacklist.md +++ /dev/null @@ -1,9 +0,0 @@ -## Access Control - -By default, access to all files is allowed (including dot files). Use `--forbid` to establish a blacklist: -```sh -$ ws --forbid '*.json' '*.yml' -serving at http://localhost:8000 -``` - -[Example](https://github.com/lwsjs/local-web-server/tree/master/example/forbid). diff --git a/doc/distribute.md b/doc/distribute.md deleted file mode 100644 index 08ca8d3..0000000 --- a/doc/distribute.md +++ /dev/null @@ -1,32 +0,0 @@ -## Distribute with your project -The standard convention with client-server applications is to add an `npm start` command to launch the server component. - -1\. Install the server as a dev dependency - -```sh -$ npm install local-web-server --save-dev -``` - -2\. Add a `start` command to your `package.json`: - -```json -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100, - "forbid": "*.json" - }, - "scripts": { - "start": "ws" - } -} -``` - -3\. Document how to build and launch your site - -```sh -$ npm install -$ npm start -serving at http://localhost:8100 -``` diff --git a/doc/https.md b/doc/https.md deleted file mode 100644 index 5867f0a..0000000 --- a/doc/https.md +++ /dev/null @@ -1,59 +0,0 @@ -## HTTPS Server - -Some modern techs (ServiceWorker, any `MediaDevices.getUserMedia()` request etc.) *must* be served from a secure origin (HTTPS). To launch an HTTPS server, supply a `--key` and `--cert` to local-web-server, for example: - -``` -$ ws --key localhost.key --cert localhost.crt -``` - -If you don't have a key and certificate it's trivial to create them. You do not need third-party verification (Verisign etc.) for development purposes. To get the green padlock in the browser, the certificate.. - -* must have a `Common Name` value matching the FQDN of the server -* must be verified by a Certificate Authority (but we can overrule this - see below) - -First create a certificate: - -1. Install openssl. - - `$ brew install openssl` - -2. Generate a RSA private key. - - `$ openssl genrsa -des3 -passout pass:x -out ws.pass.key 2048` - -3. Create RSA key. - - ``` - $ openssl rsa -passin pass:x -in ws.pass.key -out ws.key - ``` - -4. Create certificate request. The command below will ask a series of questions about the certificate owner. The most imporant answer to give is for `Common Name`, you can accept the default values for the others. **Important**: you **must** input your server's correct FQDN (`dev-server.local`, `laptop.home` etc.) into the `Common Name` field. The cert is only valid for the domain specified here. You can find out your computers host name by running the command `hostname`. For example, mine is `mba3.home`. - - `$ openssl req -new -key ws.key -out ws.csr` - -5. Generate self-signed certificate. - - `$ openssl x509 -req -days 365 -in ws.csr -signkey ws.key -out ws.crt` - -6. Clean up files we're finished with - - `$ rm ws.pass.key ws.csr` - -7. Launch HTTPS server. In iTerm, control-click the first URL (with the hostname matching `Common Name`) to launch your browser. - - ``` - $ ws --key ws.key --cert ws.crt - serving at https://mba3.home:8010, https://127.0.0.1:8010, https://192.168.1.203:8010 - ``` - -Chrome and Firefox will still complain your certificate has not been verified by a Certificate Authority. Firefox will offer you an `Add an exception` option, allowing you to ignore the warning and manually mark the certificate as trusted. In Chrome on Mac, you can manually trust the certificate another way: - -1. Open Keychain -2. Click File -> Import. Select the `.crt` file you created. -3. In the `Certificates` category, double-click the cert you imported. -4. In the `trust` section, underneath `when using this certificate`, select `Always Trust`. - -Now you have a valid, trusted certificate for development. - -### Built-in certificate -As a quick win, you can run `ws` with the `https` flag. This will launch an HTTPS server using a [built-in certificate](https://github.com/lwsjs/local-web-server/tree/master/ssl) registered to the domain 127.0.0.1. diff --git a/doc/img/logstagia.gif b/doc/img/logstagia.gif deleted file mode 100644 index 22415e0d2a5521a23acff45572b342b3df891403..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 583963 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%0DuGk=8A6W4~p9H(dFh>70XTRsC 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 #8k+3W|EGjK=(yRK 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

    FErWiaTEPv2kZ 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

    Cna`H*ViZm~&{;elS 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 web.log -``` - -In a separate tab, point goaccess at `web.log` and it will display statistics in real time: - -``` -$ goaccess -p ~/.goaccessrc -f web.log -``` - -## Logstalgia -local-web-server is compatible with [logstalgia](http://code.google.com/p/logstalgia/). - -### Install Logstalgia -On MacOSX, install with [homebrew](http://brew.sh): -```sh -$ brew install logstalgia -``` - -Alternatively, [download a release for your system from github](https://github.com/acaudwell/Logstalgia/releases/latest). - -Then pipe the `logstalgia` output format directly into logstalgia for real-time visualisation: -```sh -$ ws -f logstalgia | logstalgia - -``` - -![local-web-server with logstalgia](https://raw.githubusercontent.com/lwsjs/local-web-server/master/doc/img/logstagia.gif) - -## glTail -To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format: -```sh -$ ws -f default > web.log -``` - -Then specify this file in your glTail config: - -```yaml -servers: - dev: - host: localhost - source: local - files: /Users/Lloyd/Documents/MySite/web.log - parser: apache - color: 0.2, 0.2, 1.0, 1.0 -``` diff --git a/doc/mime-types.md b/doc/mime-types.md deleted file mode 100644 index 03d78fe..0000000 --- a/doc/mime-types.md +++ /dev/null @@ -1,12 +0,0 @@ -## mime-types -You can set additional mime-type/extension mappings, or override the defaults by setting a `mime` value in the stored config. This value is passed directly to [mime.define()](https://github.com/broofa/node-mime#mimedefine). Example: - -```json -{ - "mime": { - "text/plain": [ "php", "pl" ] - } -} -``` - -[Example](https://github.com/lwsjs/local-web-server/tree/master/example/mime-override). diff --git a/doc/mock-response.md b/doc/mock-response.md deleted file mode 100644 index a6fdc4d..0000000 --- a/doc/mock-response.md +++ /dev/null @@ -1,241 +0,0 @@ -## Mock Responses - -Mocks give you full control over the response headers and body returned to the client. They can be used to return anything from a simple html string to a resourceful REST API. Typically, they're used to mock services but can be used for anything. - -In the config, define an array called `mocks`. Each mock definition maps a [route](http://expressjs.com/guide/routing.html#route-paths) to a `response`. A simple home page: -```json -{ - "mocks": [ - { - "route": "/", - "response": { - "body": "

    Welcome to the Mock Responses example

    " - } - } - ] -} -``` - -Under the hood, the property values from the `response` object are written onto the underlying [koa response object](https://github.com/koajs/koa/blob/master/docs/api/response.md). You can set any valid koa response properies, for example [type](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsetype-1): -```json -{ - "mocks": [ - { - "route": "/", - "response": { - "type": "text/plain", - "body": "

    Welcome to the Mock Responses example

    " - } - } - ] -} -``` - -### Conditional Response - -To define a conditional response, set a `request` object on the mock definition. The `request` value acts as a query - the response defined will only be returned if each property of the `request` query matches. For example, return an XML response *only* if the request headers include `accept: application/xml`, else return 404 Not Found. - -```json -{ - "mocks": [ - { - "route": "/two", - "request": { "accepts": "xml" }, - "response": { - "body": "" - } - } - ] -} -``` - -### Multiple Potential Responses - -To specify multiple potential responses, set an array of mock definitions to the `responses` property. The first response with a matching request query will be sent. In this example, the client will get one of two responses depending on the request method: - -```json -{ - "mocks": [ - { - "route": "/three", - "responses": [ - { - "request": { "method": "GET" }, - "response": { - "body": "

    Mock response for 'GET' request on /three

    " - } - }, - { - "request": { "method": "POST" }, - "response": { - "status": 400, - "body": { "message": "That method is not allowed." } - } - } - ] - } - ] -} -``` - -### Dynamic Response - -The examples above all returned static data. To define a dynamic response, create a mock module. Specify its path in the `module` property: -```json -{ - "mocks": [ - { - "route": "/four", - "module": "/mocks/stream-self.js" - } - ] -} -``` - -Here's what the `stream-self` module looks like. The module should export a mock definition (an object, or array of objects, each with a `response` and optional `request`). In this example, the module simply streams itself to the response but you could set `body` to *any* [valid value](https://github.com/koajs/koa/blob/master/docs/api/response.md#responsebody-1). -```js -const fs = require('fs') - -module.exports = { - response: { - body: fs.createReadStream(__filename) - } -} -``` - -### Response function - -For more power, define the response as a function. It will receive the [koa context](https://github.com/koajs/koa/blob/master/docs/api/context.md) as its first argument. Now you have full programmatic control over the response returned. -```js -module.exports = { - response: function (ctx) { - ctx.body = '

    I can do anything i want.

    ' - } -} -``` - -If the route contains tokens, their values are passed to the response. For example, with this mock... -```json -{ - "mocks": [ - { - "route": "/players/:id", - "module": "/mocks/players.js" - } - ] -} -``` - -...the `id` value is passed to the `response` function. For example, a path of `/players/10?name=Lionel` would pass `10` to the response function. Additional, the value `Lionel` would be available on `ctx.query.name`: -```js -module.exports = { - response: function (ctx, id) { - ctx.body = `

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

    ` - } -} -``` - -### RESTful Resource example - -Here's an example of a REST collection (users). We'll create two routes, one for actions on the resource collection, one for individual resource actions. - -```json -{ - "mocks": [ - { "route": "/users", "module": "/mocks/users.js" }, - { "route": "/users/:id", "module": "/mocks/user.js" } - ] -} -``` - -Define a module (`users.json`) defining seed data: - -```json -[ - { "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, - { "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, - { "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } -] -``` - -The collection module: - -```js -const users = require('./users.json') - -/* responses for /users */ -const mockResponses = [ - /* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ - { request: { method: 'PUT' }, response: { status: 400 } }, - { request: { method: 'DELETE' }, response: { status: 400 } }, - { - /* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ - request: { method: 'GET' }, - response: function (ctx) { - ctx.body = users.filter(user => { - const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) - const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) - return meetsMinAge && requiredNationality - }) - } - }, - { - /* for POST requests, create a new user and return the path to the new resource */ - request: { method: 'POST' }, - response: function (ctx) { - const newUser = ctx.request.body - users.push(newUser) - newUser.id = users.length - ctx.status = 201 - ctx.response.set('Location', `/users/${newUser.id}`) - } - } -] - -module.exports = mockResponses -``` - -The individual resource module: - -```js -const users = require('./users.json') - -/* responses for /users/:id */ -const mockResponses = [ - /* don't support POST here */ - { request: { method: 'POST' }, response: { status: 400 } }, - - /* for GET requests, return a particular user */ - { - request: { method: 'GET' }, - response: function (ctx, id) { - ctx.body = users.find(user => user.id === Number(id)) - } - }, - - /* for PUT requests, update the record */ - { - request: { method: 'PUT' }, - response: function (ctx, id) { - const updatedUser = ctx.request.body - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1, updatedUser) - ctx.status = 200 - } - }, - - /* DELETE request: remove the record */ - { - request: { method: 'DELETE' }, - response: function (ctx, id) { - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1) - ctx.status = 200 - } - } -] - -module.exports = mockResponses -``` - -[Example](https://github.com/lwsjs/local-web-server/tree/master/example/mock). \ No newline at end of file diff --git a/doc/rewrite.md b/doc/rewrite.md deleted file mode 100644 index 1120a92..0000000 --- a/doc/rewrite.md +++ /dev/null @@ -1,37 +0,0 @@ -## URL rewriting - -Your application requested `/css/style.css` but it's stored at `/build/css/style.css`. To avoid a 404 you need a rewrite rule: - -```sh -$ ws --rewrite '/css/style.css -> /build/css/style.css' -``` - -Or, more generally (matching any stylesheet under `/css`): - -```sh -$ ws --rewrite '/css/:stylesheet -> /build/css/:stylesheet' -``` - -With a deep CSS directory structure it may be easier to mount the entire contents of `/build/css` to the `/css` path: - -```sh -$ ws --rewrite '/css/* -> /build/css/$1' -``` - -this rewrites `/css/a` as `/build/css/a`, `/css/a/b/c` as `/build/css/a/b/c` etc. - -### Proxied requests - -If the `to` URL contains a remote host, local-web-server will act as a proxy - fetching and responding with the remote resource. - -Mount the npm registry locally: -```sh -$ ws --rewrite '/npm/* -> http://registry.npmjs.org/$1' -``` - -Map local requests for repo data to the Github API: -```sh -$ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name' -``` - -[Example](https://github.com/lwsjs/local-web-server/tree/master/example/rewrite). diff --git a/doc/spa.md b/doc/spa.md deleted file mode 100644 index 1a05b6e..0000000 --- a/doc/spa.md +++ /dev/null @@ -1,12 +0,0 @@ -## Single Page Application - -You're building a web app with client-side routing, so mark `index.html` as the SPA. -```sh -$ ws --spa index.html -``` - -By default, typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not Found` as a file does not exist with that path. By marking `index.html` as the SPA you create this rule: - -*If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* - -[Example](https://github.com/lwsjs/local-web-server/tree/master/example/spa). diff --git a/doc/stored-config.md b/doc/stored-config.md deleted file mode 100644 index 3a63ec3..0000000 --- a/doc/stored-config.md +++ /dev/null @@ -1,28 +0,0 @@ -## Stored config - -Use the same options every time? Persist then to `package.json`: -```json -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100, - "forbid": "*.json" - } -} -``` - -or `.local-web-server.json` -```json -{ - "port": 8100, - "forbid": "*.json" -} -``` - -local-web-server will merge and use all config found, searching from the current directory upward. In the case both `package.json` and `.local-web-server.json` config is found in the same directory, `.local-web-server.json` will take precedence. Options set on the command line take precedence over all. - -To inspect stored config, run: -```sh -$ ws --config -``` \ No newline at end of file diff --git a/doc/visualisation.md b/doc/visualisation.md deleted file mode 100644 index 2ca61d9..0000000 --- a/doc/visualisation.md +++ /dev/null @@ -1,56 +0,0 @@ -## Goaccess -To get live statistics in [goaccess](http://goaccess.io/), first create this config file at `~/.goaccessrc`: - -``` -time-format %T -date-format %d/%b/%Y -log.format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" -``` - -Then, start the server, outputting `combined` format logs to disk: - -```sh -$ ws -f combined > web.log -``` - -In a separate terminal, point goaccess at `web.log` and it will display statistics in real time: - -``` -$ goaccess -p ~/.goaccessrc -f web.log -``` - -## Logstalgia -local-web-server is compatible with [logstalgia](http://code.google.com/p/logstalgia/). - -### Install Logstalgia -On MacOSX, install with [homebrew](http://brew.sh): -```sh -$ brew install logstalgia -``` - -Alternatively, [download a release for your system from github](https://github.com/acaudwell/Logstalgia/releases/latest). - -Then pipe the `logstalgia` output format directly into logstalgia for real-time visualisation: -```sh -$ ws -f logstalgia | logstalgia - -``` - -![local-web-server with logstalgia](https://raw.githubusercontent.com/lwsjs/local-web-server/master/doc/img/logstagia.gif) - -## glTail -To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format: -```sh -$ ws -f default > web.log -``` - -Then specify this file in your glTail config: - -```yaml -servers: - dev: - host: localhost - source: local - files: /Users/Lloyd/Documents/MySite/web.log - parser: apache - color: 0.2, 0.2, 1.0, 1.0 -``` diff --git a/example/built-in/forbid/.local-web-server.json b/example/built-in/forbid/.local-web-server.json deleted file mode 100644 index dd8124a..0000000 --- a/example/built-in/forbid/.local-web-server.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "forbid": [ - "/admin/*", "*.php" - ] -} diff --git a/example/built-in/forbid/admin/blocked.html b/example/built-in/forbid/admin/blocked.html deleted file mode 100644 index f51dbee..0000000 --- a/example/built-in/forbid/admin/blocked.html +++ /dev/null @@ -1 +0,0 @@ -

    Forbidden page

    diff --git a/example/built-in/forbid/allowed.html b/example/built-in/forbid/allowed.html deleted file mode 100644 index 4d0bd56..0000000 --- a/example/built-in/forbid/allowed.html +++ /dev/null @@ -1 +0,0 @@ -

    A permitted page

    diff --git a/example/built-in/forbid/index.html b/example/built-in/forbid/index.html deleted file mode 100644 index b2b30ff..0000000 --- a/example/built-in/forbid/index.html +++ /dev/null @@ -1,5 +0,0 @@ -

    Forbidden routes

    - -

    - Notice you can access this page, but not this admin page or php file. -

    diff --git a/example/built-in/forbid/something.php b/example/built-in/forbid/something.php deleted file mode 100644 index abb2fca..0000000 --- a/example/built-in/forbid/something.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/example/built-in/mime-override/.local-web-server.json b/example/built-in/mime-override/.local-web-server.json deleted file mode 100644 index d569d29..0000000 --- a/example/built-in/mime-override/.local-web-server.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "mime": { - "text/plain": [ "php" ] - } -} diff --git a/example/built-in/mime-override/something.php b/example/built-in/mime-override/something.php deleted file mode 100644 index abb2fca..0000000 --- a/example/built-in/mime-override/something.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/example/built-in/mock-async/.local-web-server.json b/example/built-in/mock-async/.local-web-server.json deleted file mode 100644 index d1168ab..0000000 --- a/example/built-in/mock-async/.local-web-server.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mocks": [ - { - "route": "/", - "module": "/mocks/delayed.js" - } - ] -} diff --git a/example/built-in/mock-async/mocks/delayed.js b/example/built-in/mock-async/mocks/delayed.js deleted file mode 100644 index ec6d325..0000000 --- a/example/built-in/mock-async/mocks/delayed.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - name: 'delayed response', - response: function (ctx) { - return new Promise((resolve, reject) => { - setTimeout(() => { - ctx.body = '

    You waited 2s for this

    ' - resolve() - }, 2000) - }) - } -} diff --git a/example/built-in/mock/.local-web-server.json b/example/built-in/mock/.local-web-server.json deleted file mode 100644 index 51c31d8..0000000 --- a/example/built-in/mock/.local-web-server.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "mocks": [ - { - "route": "/", - "response": { - "body": "

    Welcome to the Mock Responses example

    " - } - }, - { - "route": "/one", - "response": { - "type": "text/plain", - "body": "

    Welcome to the Mock Responses example

    " - } - }, - { - "route": "/two", - "request": { "accepts": "xml" }, - "response": { - "body": "" - } - }, - { - "route": "/three", - "responses": [ - { - "request": { "method": "GET" }, - "response": { - "body": "

    Mock response for 'GET' request on /three

    " - } - }, - { - "request": { "method": "POST" }, - "response": { - "status": 400, - "body": { "message": "That method is not allowed." } - } - } - ] - }, - { - "route": "/stream", - "module": "/mocks/stream-self.js" - }, - { - "route": "/five/:id", - "module": "/mocks/five.js" - }, - { - "route": "/users", - "module": "/mocks/users.js" - }, - { - "route": "/users/:id", - "module": "/mocks/user.js" - } - ] -} diff --git a/example/built-in/mock/mocks/five.js b/example/built-in/mock/mocks/five.js deleted file mode 100644 index 5165d64..0000000 --- a/example/built-in/mock/mocks/five.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - name: '/five/:id?name=:name', - response: function (ctx, id) { - ctx.body = `

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

    ` - } -} diff --git a/example/built-in/mock/mocks/stream-self.js b/example/built-in/mock/mocks/stream-self.js deleted file mode 100644 index b80018d..0000000 --- a/example/built-in/mock/mocks/stream-self.js +++ /dev/null @@ -1,8 +0,0 @@ -const fs = require('fs') - -module.exports = { - name: 'stream response', - response: { - body: fs.createReadStream(__filename) - } -} diff --git a/example/built-in/mock/mocks/user.js b/example/built-in/mock/mocks/user.js deleted file mode 100644 index 36f4d4a..0000000 --- a/example/built-in/mock/mocks/user.js +++ /dev/null @@ -1,41 +0,0 @@ -const users = require('./users.json') - -/* responses for /users/:id */ -const mockResponses = [ - /* don't support POST here */ - { request: { method: 'POST' }, response: { status: 400 } }, - - /* 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)) - } - }, - - /* for PUT requests, update the record */ - { - name: 'PUT user', - request: { method: 'PUT' }, - response: function (ctx, id) { - const updatedUser = ctx.request.body - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1, updatedUser) - ctx.status = 200 - } - }, - - /* DELETE request: remove the record */ - { - name: 'DELETE user', - request: { method: 'DELETE' }, - response: function (ctx, id) { - const existingUserIndex = users.findIndex(user => user.id === Number(id)) - users.splice(existingUserIndex, 1) - ctx.status = 200 - } - } -] - -module.exports = mockResponses diff --git a/example/built-in/mock/mocks/users.js b/example/built-in/mock/mocks/users.js deleted file mode 100644 index 8e845f2..0000000 --- a/example/built-in/mock/mocks/users.js +++ /dev/null @@ -1,32 +0,0 @@ -const users = require('./users.json') - -/* responses for /users */ -const mockResponses = [ - /* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ - { request: { method: 'PUT' }, response: { status: 400 } }, - { request: { method: 'DELETE' }, response: { status: 400 } }, - { - /* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ - request: { method: 'GET' }, - response: function (ctx) { - ctx.body = users.filter(user => { - const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) - const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) - return meetsMinAge && requiredNationality - }) - } - }, - { - /* for POST requests, create a new user and return the path to the new resource */ - request: { method: 'POST' }, - response: function (ctx) { - const newUser = ctx.request.body - users.push(newUser) - newUser.id = users.length - ctx.status = 201 - ctx.response.set('Location', `/users/${newUser.id}`) - } - } -] - -module.exports = mockResponses diff --git a/example/built-in/mock/mocks/users.json b/example/built-in/mock/mocks/users.json deleted file mode 100644 index 09a166b..0000000 --- a/example/built-in/mock/mocks/users.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - { "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, - { "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, - { "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } -] diff --git a/example/built-in/mock/mocks/users2.js b/example/built-in/mock/mocks/users2.js deleted file mode 100644 index 491aa45..0000000 --- a/example/built-in/mock/mocks/users2.js +++ /dev/null @@ -1,37 +0,0 @@ -const users = require('./users.json') - -/* responses for /users */ -const userResponses = [ - { - route: '/users', - responses: [ - /* Respond with 400 Bad Request for PUT and DELETE - inappropriate on a collection */ - { request: { method: 'PUT' }, response: { status: 400 } }, - { request: { method: 'DELETE' }, response: { status: 400 } }, - { - /* for GET requests return a subset of data, optionally filtered on 'minAge' and 'nationality' */ - request: { method: 'GET' }, - response: function (ctx) { - ctx.body = users.filter(user => { - const meetsMinAge = (user.age || 1000) >= (Number(ctx.query.minAge) || 0) - const requiredNationality = user.nationality === (ctx.query.nationality || user.nationality) - return meetsMinAge && requiredNationality - }) - } - }, - { - /* for POST requests, create a new user and return the path to the new resource */ - request: { method: 'POST' }, - response: function (ctx) { - const newUser = ctx.request.body - users.push(newUser) - newUser.id = users.length - ctx.status = 201 - ctx.response.set('Location', `/users/${newUser.id}`) - } - } - ] - } -] - -module.exports = userResponses diff --git a/example/built-in/rewrite/.local-web-server.json b/example/built-in/rewrite/.local-web-server.json deleted file mode 100644 index 1eb9003..0000000 --- a/example/built-in/rewrite/.local-web-server.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rewrite": [ - { "from": "/css/*", "to": "/build/styles/$1" }, - { "from": "/npm/*", "to": "http://registry.npmjs.org/$1" }, - { "from": "/broken/*", "to": "http://localhost:9999" }, - { "from": "/:user/repos/:name", "to": "https://api.github.com/repos/:user/:name" } - ] -} diff --git a/example/built-in/rewrite/build/styles/style.css b/example/built-in/rewrite/build/styles/style.css deleted file mode 100644 index 0a36465..0000000 --- a/example/built-in/rewrite/build/styles/style.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - font-family: monospace; - font-size: 1.3em; -} diff --git a/example/built-in/rewrite/index.html b/example/built-in/rewrite/index.html deleted file mode 100644 index 02dcfd3..0000000 --- a/example/built-in/rewrite/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -

    Rewriting paths

    - -

    Config

    -
    
    -{
    -  "rewrite": [
    -    { "from": "/css/*", "to": "/build/styles/$1" },
    -    { "from": "/npm/*", "to": "http://registry.npmjs.org/$1" },
    -    { "from": "/broken/*", "to": "http://localhost:9999" },
    -    { "from": "/:user/repos/:name", "to": "https://api.github.com/repos/:user/:name" }
    -  ]
    -}
    -
    - -

    Links

    - diff --git a/example/built-in/rewrite/lws.config.js b/example/built-in/rewrite/lws.config.js deleted file mode 100644 index 0d72234..0000000 --- a/example/built-in/rewrite/lws.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - rewrite: [ - { from: '/css/*', 'to': '/build/styles/$1' }, - { from: '/npm/*', 'to': 'http://registry.npmjs.org/$1' }, - { from: '/broken/*', 'to': 'http://localhost:9999' }, - { from: '/:user/repos/:name', 'to': 'https://api.github.com/repos/:user/:name' } - ] -} diff --git a/example/built-in/simple/css/style.css b/example/built-in/simple/css/style.css deleted file mode 100644 index 7fb71e5..0000000 --- a/example/built-in/simple/css/style.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - background-color: #AA3939; - color: #FFE2E2 -} -svg { - fill: #000 -} diff --git a/example/built-in/simple/index.html b/example/built-in/simple/index.html deleted file mode 100644 index 008f97d..0000000 --- a/example/built-in/simple/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -

    Amazing Page

    -

    - With a freaky triangle.. -

    - - - diff --git a/example/built-in/simple/package.json b/example/built-in/simple/package.json deleted file mode 100644 index 03e697a..0000000 --- a/example/built-in/simple/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100 - } -} diff --git a/example/built-in/spa/.local-web-server.json b/example/built-in/spa/.local-web-server.json deleted file mode 100644 index 4921b3c..0000000 --- a/example/built-in/spa/.local-web-server.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "spa": "index.html", - "no-cache": true -} diff --git a/example/built-in/spa/css/style.css b/example/built-in/spa/css/style.css deleted file mode 100644 index 793042d..0000000 --- a/example/built-in/spa/css/style.css +++ /dev/null @@ -1,5 +0,0 @@ -body { - background-color: IndianRed; -} - -a { color: black } diff --git a/example/built-in/spa/image.jpg b/example/built-in/spa/image.jpg deleted file mode 100644 index 953ed0f252e9037dfa3d0c64ffa24aeec96320f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2313 zcmbV~dpOkF8pqd|8ej0*gVXi9rKjFXK%>Eevdg*WpEVOdv6cl7ru9U`0n{ui!6Y>izgR1 z)g86zSbsWHZ=aj~G85du4JWCb`5<*kgm;04GT?v>SoHNu8W^kw20jFI^c8r_v`v?x z4x@y{;R!@#719hep+X%fVX#;w92SqCx(1Vgo&%f)UUPxvQi9g1twg;&l*P$e7nK<< zW%smwo{AP)1w{O)LZa&E&Z6lX7&48Ft!-@W?3XyWE^~9|czAmG{;+zD-`aKS1GfbQ zhinhsv3Fl&RCG*iT*`rihf)t8Njs5!^3>^^GiP)23oaEF6<@w`^~TNeipr|$TQ&C| zJgjeM6f`|*ZEJt_yyL~oSG|4x1A{|v-wnSPOFoZ%`8qC@O-ym2dj5fh`aj72hf4$H zQo`Y|IN}r+Mk#sL~uYvvef02C$`;+T6a0LYPbMQC<0sSH(L7AjZBB`j5C~9O?bsdVXt`3DtrOnl+ z(`GZ~P^t4x=Q9ipjf{BPK+fV2DH_NtvWYB55&cR2uVd8|*~KQWd@e zs#pv>$>9a=Fla_TGPe-8_c* zm;-@!ykTh8+{sS<^6h6E-?n`zozWKWbah8=P+ehhs9p3LyBT#xecVSQ@0SQc+^z!g zjm`+`PFgHExcZcLqN_n|O#<0Fg%y=`ne(NBC(2X%*;{(2Z~0G0gUW9DDg;jsB%1OH zci-F)%NyBxnmztBN0#R5uohV;y{4O0;?X5k-NrK>=wyHsyPZ7q??#E^{lo#0zn1gH zM>%79)r+qt_J<4$saxV4{dMEL_#-+ivPZDyk{zFIWeGfE8|?D{>9dm338DEvvxga_ z(vHlP`}8ti2S=s%+ZT8{Rnjx6Hpaf97%CnYDAv?P)QD9fVEP-svAjPpkp0f8U17A5 zTofqn-bDy*+2#DvrJ|2RL(%B7lrp)Iw+lvd;cpY850vGnDT6z=L$$4DHVoP`7 z4Tc`C@*#LNXu8U<{+2O(w;It<;nm@s2Ce;D&kRbAZ0j6M?K!TZyY zPM5D+3W3YH%Mfg7s||uc?{o!P;LCeQNMgB4xkoe8STn2m+X~gHq}uBf=&JqcwV|>o3L|lPpN7rue4=fhaqyl-Ot6rSEIgJbmv|h76MIsMzdTJ zymBC{Jdb?Taeo-~Y~Sd9Zn;loWtADl!!tl<$MMCnPCFXwSuA~N8E<$DDRbvtTX11; zk`)u;x-ykUD9`2Qe68sGCHCVJXTfN8Q=l*uX=Y0}@z%xCEkW}02QBAi9Ai2$v3>p- zuE-)z#+{M35bVq^mT#wvBjUp#@Mll%)HbkA*du}fSr$%)pyL320u=N@AU|A3N2O6_ zk6~7D;L{<*)K2b)n9|4O-4nP-dVLB6I$HG)5Pmaq^w_C}gt?O}2%2V#N)>Trg>q|R zGf(0!H$#QvPg)i+o-*@PdAbk?ic8QtPIgaV3dO1NwA^MztPp}c8jybOxzb$EMuq8c zhoE(xF*|vHe+0pG{`6@H1b5dNEx0+_+nU^ZCaERPo7m7z%?YWeYx63iiXoWq<{KZf ziBFNceX`yO!Ev8UA-^X6CcNEiaF~7jt@)tIYTnE1E3JALokN86t%z@RQKiOv!$M-A z1Lke<%gJipTius6Fbb6!0F;iiXR+v7~|KCb@iYAd|{{NQgstX&o4zf(> z(rUzl{zaWVVQl;X(U)6@6ClWlRB*VhGdr&--k#Iq?9r8Z=?$i6WY#2vT3)y{>BMWI ziK-NF5PWE;Ks2R^tH&k+ME>`Rg(WvbqWsXVem@ z^T*m3H6m`-Et%_`n0e$Qh+Q!GNY|p!WVaK8QB)s02nrhP`!BfLmzED!9r95vb03JX zV_KJ3>D(nX^@Nj44h#D3tFLaqLfq1mBCxrG&QRM#Q5s<8DgCFDjr1EnAX8#e8l!W& zj)sTq39_lR9Nk}PcF*}`&vo|U=+eAh?Dh(SL%Vs{T9YeM_T8-En(pJ~^M=Asd(5fH eixO;lF^lmc;PyMDPl2~o%TFRPL4Fb&zWg0P5bm-7 diff --git a/example/built-in/spa/index.html b/example/built-in/spa/index.html deleted file mode 100644 index 78bf089..0000000 --- a/example/built-in/spa/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - -

    Single Page App

    -

    Location:

    - -

    Found asset:

    -

    Missing asset (should 404):

    - From 008d9dbfa5231e53ec9ef6f88fcba1b9b6d093d0 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 25 Jun 2017 19:21:38 +0100 Subject: [PATCH 089/136] upgrade deps.. test --- lib/local-web-server.js | 6 +++--- package.json | 24 ++++++++++++------------ test/test.js | 9 ++++----- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 0062639..222e37b 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -2,9 +2,9 @@ const Lws = require('lws') const path = require('path') class LocalWebServer extends Lws { - constructor (options) { + create (options) { options = Object.assign({ - moduleDir: path.resolve(__dirname, `../../node_modules`), + moduleDir: path.resolve(__dirname, `../node_modules`), modulePrefix: 'lws-', stack: [ 'lws-log', @@ -22,7 +22,7 @@ class LocalWebServer extends Lws { 'lws-index' ] }, options) - super(options) + return super.create(options) } } diff --git a/package.json b/package.json index 6c1f7df..be11b8d 100644 --- a/package.json +++ b/package.json @@ -32,20 +32,20 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre.7", - "lws-blacklist": "^0.2.0", - "lws-body-parser": "^0.2.0", - "lws-compress": "^0.2.0", - "lws-conditional-get": "^0.2.0", - "lws-cors": "^0.2.0", - "lws-index": "^0.2.0", - "lws-json": "^0.2.0", - "lws-log": "^0.2.2", - "lws-mime": "^0.2.0", + "lws": "^1.0.0-pre2.2", + "lws-blacklist": "^0.2.1", + "lws-body-parser": "^0.2.1", + "lws-compress": "^0.2.1", + "lws-conditional-get": "^0.3.2", + "lws-cors": "^0.3.1", + "lws-index": "^0.3.0", + "lws-json": "^0.3.2", + "lws-log": "^0.3.0", + "lws-mime": "^0.2.1", "lws-mock-response": "^0.2.0", - "lws-rewrite": "^0.2.3", + "lws-rewrite": "^0.3.1", "lws-spa": "^0.2.0", - "lws-static": "^0.2.0" + "lws-static": "^0.3.1" }, "devDependencies": { "test-runner": "^0.4.0" diff --git a/test/test.js b/test/test.js index e529edd..be60317 100644 --- a/test/test.js +++ b/test/test.js @@ -8,13 +8,12 @@ const runner = new TestRunner() runner.test('basic', async function () { const port = 9000 + this.index - const localWebServer = new LocalWebServer({ + const localWebServer = new LocalWebServer() + const server = localWebServer.create({ port: port, - directory: 'test/fixture', - logFormat: 'none' + directory: 'test/fixture' }) - localWebServer.launch() const response = await request(`http://localhost:${port}/one.txt`) - localWebServer.server.close() + server.close() a.strictEqual(response.data.toString(), 'one\n') }) From b12606a42bbbec876c4efe11cbd0ca74027f9955 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 25 Jun 2017 23:13:30 +0100 Subject: [PATCH 090/136] deps --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index be11b8d..8f703af 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre2.2", + "lws": "^1.0.0-pre2.3", "lws-blacklist": "^0.2.1", "lws-body-parser": "^0.2.1", "lws-compress": "^0.2.1", @@ -42,10 +42,10 @@ "lws-json": "^0.3.2", "lws-log": "^0.3.0", "lws-mime": "^0.2.1", - "lws-mock-response": "^0.2.0", + "lws-mock-response": "^0.2.1", "lws-rewrite": "^0.3.1", - "lws-spa": "^0.2.0", - "lws-static": "^0.3.1" + "lws-spa": "^0.2.1", + "lws-static": "^0.3.2" }, "devDependencies": { "test-runner": "^0.4.0" From 583b9dd724b9e59a719d5c708ea082192cdc28a7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 25 Jun 2017 23:14:04 +0100 Subject: [PATCH 091/136] 2.0.0-pre2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f703af..3b62aff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre.4", + "version": "2.0.0-pre2.0", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From 4dc29ecd19119b5fc65152f575df19d16d87a7cb Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 14:24:48 +0100 Subject: [PATCH 092/136] stats.. deps --- lib/command/serve.js | 3 +++ lib/local-web-server.js | 3 +++ package.json | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/command/serve.js b/lib/command/serve.js index bde4dd4..736fc15 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -7,6 +7,9 @@ const path = require('path') class WsServe extends ServeCommand { execute (options, argv) { + const usage = require('lws/lib/usage') + usage.defaults.set('an', 'ws') + usage.defaults.set('cd4', 'cli') options = { stack: [ 'lws-log', diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 222e37b..bb2009f 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -3,6 +3,9 @@ const path = require('path') class LocalWebServer extends Lws { create (options) { + const usage = require('lws/lib/usage') + usage.defaults.set('an', 'ws') + usage.defaults.set('cd4', 'api') options = Object.assign({ moduleDir: path.resolve(__dirname, `../node_modules`), modulePrefix: 'lws-', diff --git a/package.json b/package.json index 3b62aff..1561fc8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre2.3", + "lws": "^1.0.0-pre2.4", "lws-blacklist": "^0.2.1", "lws-body-parser": "^0.2.1", "lws-compress": "^0.2.1", From c620c8f8c790e2f6c1b8b6a69d59e1b4cb07cf65 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 14:25:11 +0100 Subject: [PATCH 093/136] 2.0.0-pre2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1561fc8..4efb000 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre2.0", + "version": "2.0.0-pre2.1", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From 68a7083ab931412ab7ba48122bafbc415c59f5a9 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 18:36:56 +0100 Subject: [PATCH 094/136] Update README.md --- README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b7b392..85d2410 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,32 @@ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) [![Join the chat at https://gitter.im/lwsjs/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lwsjs/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -***Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/v1.x) for node >= v4.0.0. Documentation still WIP.*** +**This documentation is a work in progress** # local-web-server -*Documentation coming soon.* +The modular development web server for productive front-end and full-stack engineers. Use this tool to: + +* Help build a web application using any architecture (static website, Single Page Application with client-side rendering, dynamic app with server-side rendering, Progressive Web App etc.) +* Prototype a web service (REST API, microservice, websocket server application etc) + +Agnostic which front-end framework (React, Polymer, Angular etc) you use, if any. + +## Synopsis + +This package installs the `ws` command-line tool. The most simple use case is to run `ws` without any arguments - this will host the current directory as a static web site. + +```sh +$ ws +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 +``` + +Being modular and extensible, there are many possibilites how to use this tool for more advanced usage. ## Install +Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/v1.x) for node >= v4.0.0. + ```sh $ npm install -g local-web-server@next ``` From 08da58f1c1be42d1a522a4322984ee2003e4904a Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 18:41:32 +0100 Subject: [PATCH 095/136] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 85d2410..4bee9e6 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ $ ws Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` +Opening any of the listed URLs in your browser will open your home page (`index.html` by default) if one exists, else show a directory listing. + +## Advanced Usage + Being modular and extensible, there are many possibilites how to use this tool for more advanced usage. ## Install From ffb64c5bd7af1eb3b8ef9d158bb0e1e1572f5430 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 18:43:01 +0100 Subject: [PATCH 096/136] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bee9e6..9be847d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Opening any of the listed URLs in your browser will open your home page (`index. ## Advanced Usage -Being modular and extensible, there are many possibilites how to use this tool for more advanced usage. +Being modular and extensible, there are many possibilites how to use this tool. [See the wiki for full documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). ## Install From 0f4b9761789cfc5cf48a7dbea0eb828c354e402d Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 20:44:13 +0100 Subject: [PATCH 097/136] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9be847d..36a65ee 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Opening any of the listed URLs in your browser will open your home page (`index. ## Advanced Usage -Being modular and extensible, there are many possibilites how to use this tool. [See the wiki for full documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). +Being modular and extensible, features can be added or removed from `ws` in the shape of Middleware, ServerFactory or View modules. [See the wiki for full documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). ## Install From fd120ae15a5b09b24b0b5bf51512cc6fc1f305af Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 22:52:07 +0100 Subject: [PATCH 098/136] usage.. deps --- lib/command/serve.js | 2 +- package.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/command/serve.js b/lib/command/serve.js index 736fc15..0f0d55b 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -40,7 +40,7 @@ class WsServe extends ServeCommand { sections.unshift( { header: 'local-web-server', - content: 'A convenient local web server to support productive, full-stack Javascript development.' + content: 'The modular development web server for productive full-stack engineers.' }, { header: 'Synopsis', diff --git a/package.json b/package.json index 4efb000..82bf70b 100644 --- a/package.json +++ b/package.json @@ -34,18 +34,18 @@ "dependencies": { "lws": "^1.0.0-pre2.4", "lws-blacklist": "^0.2.1", - "lws-body-parser": "^0.2.1", + "lws-body-parser": "^0.2.2", "lws-compress": "^0.2.1", - "lws-conditional-get": "^0.3.2", - "lws-cors": "^0.3.1", + "lws-conditional-get": "^0.3.3", + "lws-cors": "^0.3.2", "lws-index": "^0.3.0", "lws-json": "^0.3.2", "lws-log": "^0.3.0", "lws-mime": "^0.2.1", - "lws-mock-response": "^0.2.1", - "lws-rewrite": "^0.3.1", + "lws-mock-response": "^0.2.3", + "lws-rewrite": "^0.3.2", "lws-spa": "^0.2.1", - "lws-static": "^0.3.2" + "lws-static": "^0.3.3" }, "devDependencies": { "test-runner": "^0.4.0" From 3dd00bf4c50c6dd1a8f90ee987ff3a0f5c82da0f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Mon, 26 Jun 2017 23:26:30 +0100 Subject: [PATCH 099/136] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 36a65ee..ad1f529 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ # local-web-server -The modular development web server for productive front-end and full-stack engineers. Use this tool to: +The modular development web server for productive front-end and full-stack engineers. + +Use this tool to: * Help build a web application using any architecture (static website, Single Page Application with client-side rendering, dynamic app with server-side rendering, Progressive Web App etc.) * Prototype a web service (REST API, microservice, websocket server application etc) -Agnostic which front-end framework (React, Polymer, Angular etc) you use, if any. - ## Synopsis This package installs the `ws` command-line tool. The most simple use case is to run `ws` without any arguments - this will host the current directory as a static web site. From 6db306ac368ffe3f81cb81bbdbb5bbec0af502da Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 27 Jun 2017 00:38:35 +0100 Subject: [PATCH 100/136] deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 82bf70b..bee3fc2 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,13 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre2.4", + "lws": "^1.0.0-pre2.5", "lws-blacklist": "^0.2.1", "lws-body-parser": "^0.2.2", "lws-compress": "^0.2.1", "lws-conditional-get": "^0.3.3", "lws-cors": "^0.3.2", - "lws-index": "^0.3.0", + "lws-index": "^0.3.1", "lws-json": "^0.3.2", "lws-log": "^0.3.0", "lws-mime": "^0.2.1", From 20aa3461816585bcf428630ceee8b3c74fc3ea84 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 27 Jun 2017 00:38:42 +0100 Subject: [PATCH 101/136] 2.0.0-pre2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bee3fc2..c68cb05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre2.1", + "version": "2.0.0-pre2.2", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From e06e24cdef593269dbfa059cdd2cdd94d355010c Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 27 Jun 2017 00:46:42 +0100 Subject: [PATCH 102/136] correct version in stats --- lib/command/serve.js | 6 ++++-- lib/local-web-server.js | 6 ++++-- test/test.js | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/command/serve.js b/lib/command/serve.js index 0f0d55b..b1839a4 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -8,8 +8,10 @@ const path = require('path') class WsServe extends ServeCommand { execute (options, argv) { const usage = require('lws/lib/usage') - usage.defaults.set('an', 'ws') - usage.defaults.set('cd4', 'cli') + usage.defaults + .set('an', 'ws') + .set('av', require('../../package').version) + .set('cd4', 'cli') options = { stack: [ 'lws-log', diff --git a/lib/local-web-server.js b/lib/local-web-server.js index bb2009f..67602af 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -4,8 +4,10 @@ const path = require('path') class LocalWebServer extends Lws { create (options) { const usage = require('lws/lib/usage') - usage.defaults.set('an', 'ws') - usage.defaults.set('cd4', 'api') + usage.defaults + .set('an', 'ws') + .set('av', require('../package').version) + .set('cd4', 'api') options = Object.assign({ moduleDir: path.resolve(__dirname, `../node_modules`), modulePrefix: 'lws-', diff --git a/test/test.js b/test/test.js index be60317..88427e0 100644 --- a/test/test.js +++ b/test/test.js @@ -3,6 +3,8 @@ const TestRunner = require('test-runner') const request = require('req-then') const LocalWebServer = require('../') const a = require('assert') +const usage = require('lws/lib/usage') +usage.disable() const runner = new TestRunner() From bc360ab1876f61818f07d3fad81ef9688fc51f36 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 27 Jun 2017 00:47:06 +0100 Subject: [PATCH 103/136] 2.0.0-pre2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c68cb05..b3ba1f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre2.2", + "version": "2.0.0-pre2.3", "description": "A convenient local web server to support productive, full-stack Javascript development", "bin": { "ws": "./bin/cli.js" From 9f850b8962af926746b41bbabbf58445f9b04754 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Tue, 27 Jun 2017 20:38:22 +0100 Subject: [PATCH 104/136] readme --- README.md | 8 ++++---- package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ad1f529..ada0151 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ # local-web-server -The modular development web server for productive front-end and full-stack engineers. +The modular web server for productive full-stack development. Use this tool to: -* Help build a web application using any architecture (static website, Single Page Application with client-side rendering, dynamic app with server-side rendering, Progressive Web App etc.) -* Prototype a web service (REST API, microservice, websocket server application etc) +* Build a static website, dynamic website with server-side rendering, Single Page Application with client-side rendering, Progressive Web App etc. - any web application you like. +* Prototype a REST API, Microservice, websocket server or any other server-side application. ## Synopsis @@ -33,7 +33,7 @@ Being modular and extensible, features can be added or removed from `ws` in the ## Install -Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/v1.x) for node >= v4.0.0. +Requires node v7.6 or higher. Install the [previous release](https://github.com/lwsjs/local-web-server/tree/v1.x) for node >= v4.0.0. ```sh $ npm install -g local-web-server@next diff --git a/package.json b/package.json index b3ba1f1..445d857 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "local-web-server", "version": "2.0.0-pre2.3", - "description": "A convenient local web server to support productive, full-stack Javascript development", + "description": "The modular web server for productive full-stack development", "bin": { "ws": "./bin/cli.js" }, @@ -45,7 +45,7 @@ "lws-mock-response": "^0.2.3", "lws-rewrite": "^0.3.2", "lws-spa": "^0.2.1", - "lws-static": "^0.3.3" + "lws-static": "^0.3.4" }, "devDependencies": { "test-runner": "^0.4.0" From f4abdbccaefa30d837e38f1c2c9b3513cf145f22 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 28 Jun 2017 20:13:45 +0100 Subject: [PATCH 105/136] Update README.md --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ada0151..360a32e 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,22 @@ The modular web server for productive full-stack development. Use this tool to: -* Build a static website, dynamic website with server-side rendering, Single Page Application with client-side rendering, Progressive Web App etc. - any web application you like. -* Prototype a REST API, Microservice, websocket server or any other server-side application. +* Build a fast, modern web application using any tech, framework or architecture. +* Prototype back-end services (RESTful HTTP API, Microservice, websocket server etc.) + +Essentially, local-web-server is the `lws` command-line web server with a middleware stack built in offering the following features: + +* Static file serving +* Single Page Application support +* Response mocking +* Configurable access log +* URL Rewriting, to local or remote destinations +* Request body parsing +* Route blacklisting +* HTTP Conditional Request support (cacheing) +* MIME-type customisation +* Gzip response compression +* Directory listing support ## Synopsis @@ -27,6 +41,8 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 Opening any of the listed URLs in your browser will open your home page (`index.html` by default) if one exists, else show a directory listing. + + ## Advanced Usage Being modular and extensible, features can be added or removed from `ws` in the shape of Middleware, ServerFactory or View modules. [See the wiki for full documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). From 9a34058c303bc0ef58e6246bbce1fcd7475c814c Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 28 Jun 2017 20:26:21 +0100 Subject: [PATCH 106/136] Update README.md --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 360a32e..15d97f7 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ The modular web server for productive full-stack development. Use this tool to: -* Build a fast, modern web application using any tech, framework or architecture. +* Build fast, modern web applications using any tech, framework or architecture. * Prototype back-end services (RESTful HTTP API, Microservice, websocket server etc.) -Essentially, local-web-server is the `lws` command-line web server with a middleware stack built in offering the following features: +Essentially, local-web-server is the `lws` command-line web server with a basic middleware stack built in offering the following typical features: * Static file serving * Single Page Application support @@ -39,9 +39,12 @@ $ ws Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Opening any of the listed URLs in your browser will open your home page (`index.html` by default) if one exists, else show a directory listing. - +Another common use case is to proxy certain requests to different servers (e.g. you'd like to use data from a different environment). For example, the following command would proxy `http://127.0.0.1:8000/api/users/1` to `https://internal-service.local/api/users/1`: +```sh +$ ws --rewrite '/api/* -> https://internal-service.local/api/$1` +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 +``` ## Advanced Usage From 1b97f3384b4da31e2547180a505ebd46e6f01dd0 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 28 Jun 2017 20:49:09 +0100 Subject: [PATCH 107/136] Update README.md --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 15d97f7..efb58ab 100644 --- a/README.md +++ b/README.md @@ -32,20 +32,25 @@ Essentially, local-web-server is the `lws` command-line web server with a basic ## Synopsis -This package installs the `ws` command-line tool. The most simple use case is to run `ws` without any arguments - this will host the current directory as a static web site. +This package installs the `ws` command-line tool. The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. ```sh $ ws Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Another common use case is to proxy certain requests to different servers (e.g. you'd like to use data from a different environment). For example, the following command would proxy `http://127.0.0.1:8000/api/users/1` to `https://internal-service.local/api/users/1`: +Another common use case is to **proxy certain requests to remote servers** (e.g. you'd like to use data from a different environment). For example, the following command would proxy `http://127.0.0.1:8000/api/users/1` to `https://internal-service.local/api/users/1`: ```sh $ ws --rewrite '/api/* -> https://internal-service.local/api/$1` Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` +Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, you could use Mock Responses to fill the gap. Define the mock responses in a module. + +```js +``` + ## Advanced Usage Being modular and extensible, features can be added or removed from `ws` in the shape of Middleware, ServerFactory or View modules. [See the wiki for full documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). From 366d26216ccf3eadc4d02e050e0d50db29367406 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 28 Jun 2017 21:06:37 +0100 Subject: [PATCH 108/136] Update README.md --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index efb58ab..e2f6a05 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,38 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, you could use Mock Responses to fill the gap. Define the mock responses in a module. ```js +const users = [ + { "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, + { "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, + { "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } +] + +/* response mocks for /users */ +module.exports = [ + { + route: '/users', + responses: [ + /* Respond with 400 Bad Request for PUT and DELETE requests (inappropriate on a collection) */ + { request: { method: 'PUT' }, response: { status: 400 } }, + { request: { method: 'DELETE' }, response: { status: 400 } }, + { + /* for GET requests return the collection */ + request: { method: 'GET' }, + response: { type: 'application/json', body: users } + }, + { + /* for POST requests, create a new user and return its location */ + request: { method: 'POST' }, + response: function (ctx) { + const newUser = ctx.request.body + users.push(newUser) + ctx.status = 201 + ctx.response.set('Location', `/users/${users.length}`) + } + } + ] + } +] ``` ## Advanced Usage From 61daf95db986e90106797102afc3d014ddae9591 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 28 Jun 2017 21:12:43 +0100 Subject: [PATCH 109/136] Update README.md --- README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e2f6a05..d6043e0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Essentially, local-web-server is the `lws` command-line web server with a basic ## Synopsis -This package installs the `ws` command-line tool. The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. +This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. ```sh $ ws @@ -83,6 +83,38 @@ module.exports = [ ] ``` +Next, launch `ws` passing in your mock response file: + +```sh +$ ws --mocks example-mocks.js +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 +``` + +Test your mock responses: + +```sh +$ curl http://127.0.0.1:8000/users +[ + { + "id": 1, + "name": "Lloyd", + "age": 40, + "nationality": "English" + }, + { + "id": 2, + "name": "Mona", + "age": 34, + "nationality": "Palestinian" + }, + { + "id": 3, + "name": "Francesco", + "age": 24, + "nationality": "Italian" + } +``` + ## Advanced Usage Being modular and extensible, features can be added or removed from `ws` in the shape of Middleware, ServerFactory or View modules. [See the wiki for full documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). From fd5f731ce20ca1c4146a72dcad9a71e53fd9ccb8 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 28 Jun 2017 21:33:06 +0100 Subject: [PATCH 110/136] Update README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index d6043e0..79ec157 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,15 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 Test your mock responses: ```sh +$ curl http://127.0.0.1:8000/users -H 'Content-type: application/json' -d '{ "name": "Anthony" }' -i +HTTP/1.1 201 Created +Vary: Origin +Location: /users/4 +Content-Type: text/plain; charset=utf-8 +Content-Length: 7 +Date: Wed, 28 Jun 2017 20:31:19 GMT +Connection: keep-alive + $ curl http://127.0.0.1:8000/users [ { @@ -112,6 +121,10 @@ $ curl http://127.0.0.1:8000/users "name": "Francesco", "age": 24, "nationality": "Italian" + }, + { + "id": 4, + "name": "Anthony" } ``` From a992c465ae5d51080858357a9a109351fd2f0781 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 28 Jun 2017 21:37:44 +0100 Subject: [PATCH 111/136] Update README.md --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 79ec157..e1397c4 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,17 @@ Use this tool to: * Build fast, modern web applications using any tech, framework or architecture. * Prototype back-end services (RESTful HTTP API, Microservice, websocket server etc.) -Essentially, local-web-server is the `lws` command-line web server with a basic middleware stack built in offering the following typical features: +Features: -* Static file serving +* HTTP, HTTPS and HTTP2 support +* Create, share and consume middleware, view and server modules +* URL Rewriting, to local or remote destinations * Single Page Application support * Response mocking * Configurable access log -* URL Rewriting, to local or remote destinations -* Request body parsing * Route blacklisting * HTTP Conditional Request support (cacheing) -* MIME-type customisation -* Gzip response compression -* Directory listing support +* Gzip response compression and much more ## Synopsis From 1b7d8eff40a50586f91bf63439e1169c891e43d5 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 29 Jun 2017 12:31:16 +0100 Subject: [PATCH 112/136] upgrade deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 445d857..2dfa744 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre2.5", + "lws": "^1.0.0-pre2.6", "lws-blacklist": "^0.2.1", "lws-body-parser": "^0.2.2", "lws-compress": "^0.2.1", @@ -43,7 +43,7 @@ "lws-log": "^0.3.0", "lws-mime": "^0.2.1", "lws-mock-response": "^0.2.3", - "lws-rewrite": "^0.3.2", + "lws-rewrite": "^0.3.4", "lws-spa": "^0.2.1", "lws-static": "^0.3.4" }, From 56ebc2a652d776c82a2e74f60f274c3ce8ab3bef Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 29 Jun 2017 12:31:26 +0100 Subject: [PATCH 113/136] 2.0.0-pre2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2dfa744..4e7d9b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre2.3", + "version": "2.0.0-pre2.4", "description": "The modular web server for productive full-stack development", "bin": { "ws": "./bin/cli.js" From 7db99283b8564b6c258c9b249f00aeb9131ab69a Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 1 Jul 2017 10:43:00 +0100 Subject: [PATCH 114/136] Update README.md --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e1397c4..5bd6418 100644 --- a/README.md +++ b/README.md @@ -15,29 +15,30 @@ Use this tool to: * Build fast, modern web applications using any tech, framework or architecture. * Prototype back-end services (RESTful HTTP API, Microservice, websocket server etc.) +* Monitor activity, analyse performance, compare caching strategies etc. Features: * HTTP, HTTPS and HTTP2 support -* Create, share and consume middleware, view and server modules -* URL Rewriting, to local or remote destinations +* Modular. Create, share and consume middleware, view and server plugins. +* URL Rewriting to local or remote destinations * Single Page Application support -* Response mocking +* Response mocking * Configurable access log * Route blacklisting -* HTTP Conditional Request support (cacheing) +* HTTP Conditional Request support * Gzip response compression and much more ## Synopsis -This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. +This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. Navigating to the server will render your `index.html` or show a directory listing, if you don't have one. ```sh $ ws Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Another common use case is to **proxy certain requests to remote servers** (e.g. you'd like to use data from a different environment). For example, the following command would proxy `http://127.0.0.1:8000/api/users/1` to `https://internal-service.local/api/users/1`: +Another common use case is to **proxy certain requests to a remote server** if, for example, you'd like to use data from a different environment. The following command would proxy requests with a URL beginning with `http://127.0.0.1:8000/api/` to `https://internal-service.local/api/`: ```sh $ ws --rewrite '/api/* -> https://internal-service.local/api/$1` From 27a32bf6cf0824a2b8430cad6ee7fbf9350e94d1 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 1 Jul 2017 10:48:14 +0100 Subject: [PATCH 115/136] Update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5bd6418..b798a08 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ $ ws --rewrite '/api/* -> https://internal-service.local/api/$1` Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, you could use Mock Responses to fill the gap. Define the mock responses in a module. +Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, Mock Responses can fill the gap. Export your mock responses from a module. ```js const users = [ - { "id": 1, "name": "Lloyd", "age": 40, "nationality": "English" }, - { "id": 2, "name": "Mona", "age": 34, "nationality": "Palestinian" }, - { "id": 3, "name": "Francesco", "age": 24, "nationality": "Italian" } + { "id": 1, "name": "Lloyd", "age": 40 }, + { "id": 2, "name": "Mona", "age": 34 }, + { "id": 3, "name": "Francesco", "age": 24 } ] /* response mocks for /users */ @@ -89,7 +89,7 @@ $ ws --mocks example-mocks.js Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Test your mock responses: +Test your mock responses. A `POST` request should return a `201` with a `Location` header and empty body. ```sh $ curl http://127.0.0.1:8000/users -H 'Content-type: application/json' -d '{ "name": "Anthony" }' -i @@ -100,7 +100,11 @@ Content-Type: text/plain; charset=utf-8 Content-Length: 7 Date: Wed, 28 Jun 2017 20:31:19 GMT Connection: keep-alive +``` + +A `GET` to `/users` should return our mock user data, including the record just added. +```sh $ curl http://127.0.0.1:8000/users [ { From 81f90e7d851077ecf0d8c2f8ed484da4f4a9896f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 1 Jul 2017 10:52:59 +0100 Subject: [PATCH 116/136] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b798a08..bec55e4 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,9 @@ $ curl http://127.0.0.1:8000/users } ``` -## Advanced Usage +## Further Documentation -Being modular and extensible, features can be added or removed from `ws` in the shape of Middleware, ServerFactory or View modules. [See the wiki for full documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). +[See the wiki for plenty more documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). ## Install From b1bc8bed9613be3386bda616ec0e07e3bc0ed24b Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 1 Jul 2017 23:08:30 +0100 Subject: [PATCH 117/136] add lws-request-monitor --- README.md | 7 ++++--- lib/command/middleware-list.js | 1 + lib/command/serve.js | 1 + lib/local-web-server.js | 1 + package.json | 7 ++++--- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bec55e4..e748295 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 Another common use case is to **proxy certain requests to a remote server** if, for example, you'd like to use data from a different environment. The following command would proxy requests with a URL beginning with `http://127.0.0.1:8000/api/` to `https://internal-service.local/api/`: ```sh -$ ws --rewrite '/api/* -> https://internal-service.local/api/$1` +$ ws --rewrite '/api/* -> https://internal-service.local/api/$1' Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` @@ -73,8 +73,9 @@ module.exports = [ response: function (ctx) { const newUser = ctx.request.body users.push(newUser) + newUser.id = users.length ctx.status = 201 - ctx.response.set('Location', `/users/${users.length}`) + ctx.response.set('Location', `/users/${newUser.id}`) } } ] @@ -82,7 +83,7 @@ module.exports = [ ] ``` -Next, launch `ws` passing in your mock response file: +Next, launch `ws` passing in your mock response file: ```sh $ ws --mocks example-mocks.js diff --git a/lib/command/middleware-list.js b/lib/command/middleware-list.js index caf2f65..ed622a6 100644 --- a/lib/command/middleware-list.js +++ b/lib/command/middleware-list.js @@ -4,6 +4,7 @@ class MiddlewareList { } execute (options) { const list = [ + 'lws-request-monitor', 'lws-log', 'lws-cors', 'lws-json', diff --git a/lib/command/serve.js b/lib/command/serve.js index b1839a4..d81d2d7 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -14,6 +14,7 @@ class WsServe extends ServeCommand { .set('cd4', 'cli') options = { stack: [ + 'lws-request-monitor', 'lws-log', 'lws-cors', 'lws-json', diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 67602af..92ac13d 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -12,6 +12,7 @@ class LocalWebServer extends Lws { moduleDir: path.resolve(__dirname, `../node_modules`), modulePrefix: 'lws-', stack: [ + 'lws-request-monitor', 'lws-log', 'lws-cors', 'lws-json', diff --git a/package.json b/package.json index 4e7d9b4..0367ca6 100644 --- a/package.json +++ b/package.json @@ -32,19 +32,20 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre2.6", + "lws": "^1.0.0-pre2.7", "lws-blacklist": "^0.2.1", "lws-body-parser": "^0.2.2", "lws-compress": "^0.2.1", "lws-conditional-get": "^0.3.3", "lws-cors": "^0.3.2", - "lws-index": "^0.3.1", + "lws-index": "^0.3.2", "lws-json": "^0.3.2", "lws-log": "^0.3.0", "lws-mime": "^0.2.1", "lws-mock-response": "^0.2.3", + "lws-request-monitor": "^0.1.0", "lws-rewrite": "^0.3.4", - "lws-spa": "^0.2.1", + "lws-spa": "^0.2.2", "lws-static": "^0.3.4" }, "devDependencies": { From 22bc5df12c3603b375e2def30f48c0f73d947363 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Wed, 5 Jul 2017 12:49:26 +0100 Subject: [PATCH 118/136] default-stack module --- lib/command/middleware-list.js | 17 +---------------- lib/command/serve.js | 17 +---------------- lib/default-stack.js | 16 ++++++++++++++++ lib/local-web-server.js | 17 +---------------- package.json | 2 +- 5 files changed, 20 insertions(+), 49 deletions(-) create mode 100644 lib/default-stack.js diff --git a/lib/command/middleware-list.js b/lib/command/middleware-list.js index ed622a6..557efc4 100644 --- a/lib/command/middleware-list.js +++ b/lib/command/middleware-list.js @@ -3,22 +3,7 @@ class MiddlewareList { return 'Print available middleware' } execute (options) { - const list = [ - 'lws-request-monitor', - 'lws-log', - 'lws-cors', - 'lws-json', - 'lws-rewrite', - 'lws-body-parser', - 'lws-blacklist', - 'lws-conditional-get', - 'lws-mime', - 'lws-compress', - 'lws-mock-response', - 'lws-spa', - 'lws-static', - 'lws-index' - ] + const list = require('../default-stack') console.log(list) } } diff --git a/lib/command/serve.js b/lib/command/serve.js index d81d2d7..b66c189 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -13,22 +13,7 @@ class WsServe extends ServeCommand { .set('av', require('../../package').version) .set('cd4', 'cli') options = { - stack: [ - 'lws-request-monitor', - 'lws-log', - 'lws-cors', - 'lws-json', - 'lws-rewrite', - 'lws-body-parser', - 'lws-blacklist', - 'lws-conditional-get', - 'lws-mime', - 'lws-compress', - 'lws-mock-response', - 'lws-spa', - 'lws-static', - 'lws-index' - ], + stack: require('../default-stack'), moduleDir: path.resolve(__dirname, `../../node_modules`), modulePrefix: 'lws-' } diff --git a/lib/default-stack.js b/lib/default-stack.js new file mode 100644 index 0000000..7d2082f --- /dev/null +++ b/lib/default-stack.js @@ -0,0 +1,16 @@ +module.exports = [ + 'lws-body-parser', + 'lws-request-monitor', + 'lws-log', + 'lws-cors', + 'lws-json', + 'lws-rewrite', + 'lws-blacklist', + 'lws-conditional-get', + 'lws-mime', + 'lws-compress', + 'lws-mock-response', + 'lws-spa', + 'lws-static', + 'lws-index' +] diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 92ac13d..5455322 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -11,22 +11,7 @@ class LocalWebServer extends Lws { options = Object.assign({ moduleDir: path.resolve(__dirname, `../node_modules`), modulePrefix: 'lws-', - stack: [ - 'lws-request-monitor', - 'lws-log', - 'lws-cors', - 'lws-json', - 'lws-rewrite', - 'lws-body-parser', - 'lws-blacklist', - 'lws-conditional-get', - 'lws-mime', - 'lws-compress', - 'lws-mock-response', - 'lws-spa', - 'lws-static', - 'lws-index' - ] + stack: require('./default-stack') }, options) return super.create(options) } diff --git a/package.json b/package.json index 0367ca6..a7462d9 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "lws-json": "^0.3.2", "lws-log": "^0.3.0", "lws-mime": "^0.2.1", - "lws-mock-response": "^0.2.3", + "lws-mock-response": "^0.2.4", "lws-request-monitor": "^0.1.0", "lws-rewrite": "^0.3.4", "lws-spa": "^0.2.2", From b2e72d86af3610b28e112747a5d666fc205b0699 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 09:39:22 +0100 Subject: [PATCH 119/136] Update README.md --- README.md | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e748295..2f24b1f 100644 --- a/README.md +++ b/README.md @@ -54,33 +54,37 @@ const users = [ { "id": 3, "name": "Francesco", "age": 24 } ] -/* response mocks for /users */ -module.exports = [ - { - route: '/users', - responses: [ - /* Respond with 400 Bad Request for PUT and DELETE requests (inappropriate on a collection) */ - { request: { method: 'PUT' }, response: { status: 400 } }, - { request: { method: 'DELETE' }, response: { status: 400 } }, - { - /* for GET requests return the collection */ - request: { method: 'GET' }, - response: { type: 'application/json', body: users } - }, +module.exports = MockBase => class MockUsers extends MockBase { + mocks () { + /* response mocks for /users */ + return [ { - /* for POST requests, create a new user and return its location */ - request: { method: 'POST' }, - response: function (ctx) { - const newUser = ctx.request.body - users.push(newUser) - newUser.id = users.length - ctx.status = 201 - ctx.response.set('Location', `/users/${newUser.id}`) - } + route: '/users', + responses: [ + /* Respond with 400 Bad Request for PUT and DELETE requests (inappropriate on a collection) */ + { request: { method: 'PUT' }, response: { status: 400 } }, + { request: { method: 'DELETE' }, response: { status: 400 } }, + { + /* for GET requests return the collection */ + request: { method: 'GET' }, + response: { type: 'application/json', body: users } + }, + { + /* for POST requests, create a new user and return its location */ + request: { method: 'POST' }, + response: function (ctx) { + const newUser = ctx.request.body + users.push(newUser) + newUser.id = users.length + ctx.status = 201 + ctx.response.set('Location', `/users/${newUser.id}`) + } + } + ] } ] } -] +} ``` Next, launch `ws` passing in your mock response file: From c0b078952ed7938ef8ffe8be2ec032462a4a89db Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 13:27:17 +0100 Subject: [PATCH 120/136] readme, deps --- README.md | 11 ++++------- package.json | 6 +++--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2f24b1f..173bfe1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Features: ## Synopsis -This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. Navigating to the server will render your `index.html` or show a directory listing, if you don't have one. +This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. Navigating to the server will render a directory listing or your `index.html`, if that file exists. ```sh $ ws @@ -115,20 +115,17 @@ $ curl http://127.0.0.1:8000/users { "id": 1, "name": "Lloyd", - "age": 40, - "nationality": "English" + "age": 40 }, { "id": 2, "name": "Mona", - "age": 34, - "nationality": "Palestinian" + "age": 34 }, { "id": 3, "name": "Francesco", - "age": 24, - "nationality": "Italian" + "age": 24 }, { "id": 4, diff --git a/package.json b/package.json index a7462d9..96dd249 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,9 @@ "lws-json": "^0.3.2", "lws-log": "^0.3.0", "lws-mime": "^0.2.1", - "lws-mock-response": "^0.2.4", - "lws-request-monitor": "^0.1.0", - "lws-rewrite": "^0.3.4", + "lws-mock-response": "^0.3.0", + "lws-request-monitor": "^0.1.2", + "lws-rewrite": "^0.3.5", "lws-spa": "^0.2.2", "lws-static": "^0.3.4" }, From f186a3320499b78db7d69dbba4a0ad8f325dedf4 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 20:25:59 +0100 Subject: [PATCH 121/136] readme --- README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 173bfe1..a45356f 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ The modular web server for productive full-stack development. Use this tool to: * Build fast, modern web applications using any tech, framework or architecture. -* Prototype back-end services (RESTful HTTP API, Microservice, websocket server etc.) -* Monitor activity, analyse performance, compare caching strategies etc. +* Prototype back-end services (RESTful HTTP API, Microservice, websocket server, Server Sent Events etc.) +* Monitor activity, analyse performance, experiment with caching strategies etc. Features: @@ -31,20 +31,41 @@ Features: ## Synopsis -This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. Navigating to the server will render a directory listing or your `index.html`, if that file exists. +This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). + +#### Static web site + +The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. Navigating to the server will render a directory listing or your `index.html`, if that file exists. ```sh $ ws Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Another common use case is to **proxy certain requests to a remote server** if, for example, you'd like to use data from a different environment. The following command would proxy requests with a URL beginning with `http://127.0.0.1:8000/api/` to `https://internal-service.local/api/`: +#### Single Page Application + +Serving a Single Page Application is as trivial as specifying the name of your single page: + +```sh +$ ws --spa index.html +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 +``` + +By default, requests for typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not Found` as a file at that locaiton does not exist. By marking `index.html` as the SPA you create this rule: + +*If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* + +#### URL rewriting and proxied requests + +Another common use case is to **re-route certain requests to a remote server** if, for example, you'd like to use data from a different environment. The following command would proxy requests with a URL beginning with `http://127.0.0.1:8000/api/` to `https://internal-service.local/api/`: ```sh $ ws --rewrite '/api/* -> https://internal-service.local/api/$1' Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` +#### Mock responses + Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, Mock Responses can fill the gap. Export your mock responses from a module. ```js From 040a6ee030c4a7d5ba1e5cf27d677b81648aa065 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 20:35:50 +0100 Subject: [PATCH 122/136] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a45356f..280a7a1 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Features: This package installs the `ws` command-line tool (take a look at the [usage guide](https://github.com/lwsjs/local-web-server/wiki/CLI-usage)). -#### Static web site +### Static web site The most simple use case is to run `ws` without any arguments - this will **host the current directory as a static web site**. Navigating to the server will render a directory listing or your `index.html`, if that file exists. @@ -42,7 +42,7 @@ $ ws Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -#### Single Page Application +### Single Page Application Serving a Single Page Application is as trivial as specifying the name of your single page: @@ -55,7 +55,7 @@ By default, requests for typical SPA paths (e.g. `/user/1`, `/login`) would retu *If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* -#### URL rewriting and proxied requests +### URL rewriting and proxied requests Another common use case is to **re-route certain requests to a remote server** if, for example, you'd like to use data from a different environment. The following command would proxy requests with a URL beginning with `http://127.0.0.1:8000/api/` to `https://internal-service.local/api/`: @@ -64,7 +64,7 @@ $ ws --rewrite '/api/* -> https://internal-service.local/api/$1' Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -#### Mock responses +### Mock responses Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, Mock Responses can fill the gap. Export your mock responses from a module. @@ -108,7 +108,7 @@ module.exports = MockBase => class MockUsers extends MockBase { } ``` -Next, launch `ws` passing in your mock response file: +Next, launch `ws` passing in your mocks module: ```sh $ ws --mocks example-mocks.js From 15363ef472e2a612559554a94d3b2aa011be823f Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 21:14:45 +0100 Subject: [PATCH 123/136] readme --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 280a7a1..4903c65 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,15 @@ The modular web server for productive full-stack development. Use this tool to: -* Build fast, modern web applications using any tech, framework or architecture. -* Prototype back-end services (RESTful HTTP API, Microservice, websocket server, Server Sent Events etc.) +* Build any flavour of web application (static site, dynamic site with client or server-rendered content, Single Page Apps, Progessive Web Apps, Angular or React apps etc.) +* Prototype any CORS-enabled back-end service (e.g. RESTful HTTP API or Microservice using websockets, Server Sent Events etc.) * Monitor activity, analyse performance, experiment with caching strategies etc. Features: +* Modular, extensible and easy to personalise. Create, share and consume the plugins which match your requirements. +* Powerful, extensible command-line interface (add your own options) * HTTP, HTTPS and HTTP2 support -* Modular. Create, share and consume middleware, view and server plugins. * URL Rewriting to local or remote destinations * Single Page Application support * Response mocking @@ -44,7 +45,7 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 ### Single Page Application -Serving a Single Page Application is as trivial as specifying the name of your single page: +Serving a Single Page Application (e.g. a React or Angular app) is as trivial as specifying the name of your single page: ```sh $ ws --spa index.html @@ -154,6 +155,15 @@ $ curl http://127.0.0.1:8000/users } ``` +### HTTPS + +Launching a secure server is as simple as setting the `--https` flag. [See the wiki](https://github.com/lwsjs/local-web-server/wiki) for further configuration options and a guide on how to get the "green padlock" in your browser. + +```sh +$ ws --https +Serving at https://mbp.local:8000, https://127.0.0.1:8000, https://192.168.0.100:8000 +``` + ## Further Documentation [See the wiki for plenty more documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). From a073f618dc63a3f7fe35abf5176701945956d759 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 21:18:10 +0100 Subject: [PATCH 124/136] readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4903c65..3461975 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ The modular web server for productive full-stack development. Use this tool to: -* Build any flavour of web application (static site, dynamic site with client or server-rendered content, Single Page Apps, Progessive Web Apps, Angular or React apps etc.) +* Build any flavour of web application (static site, dynamic site with client or server-rendered content, Single Page App, Progessive Web App, Angular or React app etc.) * Prototype any CORS-enabled back-end service (e.g. RESTful HTTP API or Microservice using websockets, Server Sent Events etc.) * Monitor activity, analyse performance, experiment with caching strategies etc. Features: * Modular, extensible and easy to personalise. Create, share and consume the plugins which match your requirements. -* Powerful, extensible command-line interface (add your own options) -* HTTP, HTTPS and HTTP2 support +* Powerful, extensible command-line interface (add your own commands and options) +* HTTP, HTTPS and experimental HTTP2 support * URL Rewriting to local or remote destinations * Single Page Application support * Response mocking From fc2bc5a92a15740e570f8d99fdb15d204125f731 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 23:52:48 +0100 Subject: [PATCH 125/136] tests --- .coveralls.yml | 2 +- README.md | 2 +- lib/command/serve.js | 2 +- package.json | 4 +++- test/cli.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 test/cli.js diff --git a/.coveralls.yml b/.coveralls.yml index d007aa8..0d01e20 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -repo_token: w9HmlMl9558e1LpP9p62YgYutkVE9PqtN +repo_token: K4pavPyoEIHgj3bxfghHu2YmA8aqrnAnA diff --git a/README.md b/README.md index 3461975..020810b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![npm (tag)](https://img.shields.io/npm/v/local-web-server/next.svg)](https://www.npmjs.org/package/local-web-server) [![npm module downloads](https://img.shields.io/npm/dt/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) [![Build Status](https://travis-ci.org/lwsjs/local-web-server.svg?branch=next)](https://travis-ci.org/lwsjs/local-web-server) -[![Dependency Status](https://david-dm.org/lwsjs/local-web-server/next.svg)](https://david-dm.org/lwsjs/local-web-server/next) +[![Coverage Status](https://coveralls.io/repos/github/lwsjs/local-web-server/badge.svg?branch=next)](https://coveralls.io/github/lwsjs/local-web-server?branch=next)[![Dependency Status](https://david-dm.org/lwsjs/local-web-server/next.svg)](https://david-dm.org/lwsjs/local-web-server/next) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) [![Join the chat at https://gitter.im/lwsjs/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lwsjs/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/lib/command/serve.js b/lib/command/serve.js index b66c189..d5a0aa9 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -17,7 +17,7 @@ class WsServe extends ServeCommand { moduleDir: path.resolve(__dirname, `../../node_modules`), modulePrefix: 'lws-' } - super.execute(options, argv) + return super.execute(options, argv) } usage () { diff --git a/package.json b/package.json index 96dd249..4e7a2f0 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "scripts": { "test": "test-runner test/*.js", "docs": "jsdoc2md -t jsdoc2md/api.hbs -p list lib/*.js > doc/api.md; echo", - "cover": "istanbul cover ./node_modules/.bin/tape test/*.js && cat coverage/lcov.info | coveralls && rm -rf coverage; echo" + "cover": "istanbul cover ./node_modules/.bin/test-runner test/*.js && cat coverage/lcov.info | coveralls" }, "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", @@ -49,6 +49,8 @@ "lws-static": "^0.3.4" }, "devDependencies": { + "coveralls": "^2.13.1", + "req-then": "^0.6.4", "test-runner": "^0.4.0" } } diff --git a/test/cli.js b/test/cli.js new file mode 100644 index 0000000..c835f9f --- /dev/null +++ b/test/cli.js @@ -0,0 +1,49 @@ +const TestRunner = require('test-runner') +const a = require('assert') +const CliApp = require('../lib/cli-app') +const request = require('req-then') +const usage = require('lws/lib/usage') +usage.disable() + +const runner = new TestRunner() + +runner.test('cli.run', async function () { + const port = 7500 + this.index + const origArgv = process.argv.slice() + process.argv = [ 'node', 'something', '--port', `${port}` ] + const server = CliApp.run() + process.argv = origArgv + const response = await request(`http://127.0.0.1:${port}/`) + server.close() + a.strictEqual(response.res.statusCode, 200) +}) + +runner.test('cli.run: bad option', async function () { + const port = 7500 + this.index + const origArgv = process.argv.slice() + process.argv = [ 'node', 'something', '--should-fail' ] + const server = CliApp.run() + process.argv = origArgv + a.strictEqual(server, undefined) +}) + +runner.test('cli.run: --help', async function () { + const origArgv = process.argv.slice() + process.argv = [ 'node', 'something', '--help' ] + CliApp.run() + process.argv = origArgv +}) + +runner.test('cli.run: --version', async function () { + const origArgv = process.argv.slice() + process.argv = [ 'node', 'something', '--version' ] + CliApp.run() + process.argv = origArgv +}) + +runner.test('cli.run: middleware-list', async function () { + const origArgv = process.argv.slice() + process.argv = [ 'node', 'something', 'middleware-list' ] + CliApp.run() + process.argv = origArgv +}) From 2b76fc9f4f39fa708a9249ef95b10f7ecd423321 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Fri, 7 Jul 2017 23:58:43 +0100 Subject: [PATCH 126/136] readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 020810b..f2ab3e6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ [![npm (tag)](https://img.shields.io/npm/v/local-web-server/next.svg)](https://www.npmjs.org/package/local-web-server) [![npm module downloads](https://img.shields.io/npm/dt/local-web-server.svg)](https://www.npmjs.org/package/local-web-server) [![Build Status](https://travis-ci.org/lwsjs/local-web-server.svg?branch=next)](https://travis-ci.org/lwsjs/local-web-server) -[![Coverage Status](https://coveralls.io/repos/github/lwsjs/local-web-server/badge.svg?branch=next)](https://coveralls.io/github/lwsjs/local-web-server?branch=next)[![Dependency Status](https://david-dm.org/lwsjs/local-web-server/next.svg)](https://david-dm.org/lwsjs/local-web-server/next) +[![Coverage Status](https://coveralls.io/repos/github/lwsjs/local-web-server/badge.svg?branch=next)](https://coveralls.io/github/lwsjs/local-web-server?branch=next) +[![Dependency Status](https://david-dm.org/lwsjs/local-web-server/next.svg)](https://david-dm.org/lwsjs/local-web-server/next) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) [![Join the chat at https://gitter.im/lwsjs/local-web-server](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lwsjs/local-web-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From 890ee5c4aadb6f233ae3c6cd929b9e72335ce1c4 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 00:21:03 +0100 Subject: [PATCH 127/136] deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4e7a2f0..b074d65 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre2.7", + "lws": "^1.0.0-pre3.1", "lws-blacklist": "^0.2.1", "lws-body-parser": "^0.2.2", "lws-compress": "^0.2.1", @@ -40,13 +40,13 @@ "lws-cors": "^0.3.2", "lws-index": "^0.3.2", "lws-json": "^0.3.2", - "lws-log": "^0.3.0", + "lws-log": "^0.3.1", "lws-mime": "^0.2.1", - "lws-mock-response": "^0.3.0", - "lws-request-monitor": "^0.1.2", + "lws-mock-response": "^0.4.0", + "lws-request-monitor": "^0.1.3", "lws-rewrite": "^0.3.5", "lws-spa": "^0.2.2", - "lws-static": "^0.3.4" + "lws-static": "^0.4.0" }, "devDependencies": { "coveralls": "^2.13.1", From 1f76a592a47969ad22267dba3d962cd6bec7d850 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 00:21:38 +0100 Subject: [PATCH 128/136] 2.0.0-pre3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b074d65..9cec93e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre2.4", + "version": "2.0.0-pre3.0", "description": "The modular web server for productive full-stack development", "bin": { "ws": "./bin/cli.js" From ed7f55e4bd24b07547e2c2a8c83939ed5299e014 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 11:16:49 +0100 Subject: [PATCH 129/136] readme --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f2ab3e6..75b4e1b 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,11 @@ Use this tool to: * Build any flavour of web application (static site, dynamic site with client or server-rendered content, Single Page App, Progessive Web App, Angular or React app etc.) * Prototype any CORS-enabled back-end service (e.g. RESTful HTTP API or Microservice using websockets, Server Sent Events etc.) * Monitor activity, analyse performance, experiment with caching strategies etc. +* Build your own, personalised CLI web server tool Features: -* Modular, extensible and easy to personalise. Create, share and consume the plugins which match your requirements. +* Modular, extensible and easy to personalise. Create, share and consume only plugins which match your requirements. * Powerful, extensible command-line interface (add your own commands and options) * HTTP, HTTPS and experimental HTTP2 support * URL Rewriting to local or remote destinations @@ -46,14 +47,14 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 ### Single Page Application -Serving a Single Page Application (e.g. a React or Angular app) is as trivial as specifying the name of your single page: +Serving a Single Page Application (an app with client-side routing, e.g. a React or Angular app) is as trivial as specifying the name of your single page: ```sh $ ws --spa index.html Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -By default, requests for typical SPA paths (e.g. `/user/1`, `/login`) would return `404 Not Found` as a file at that locaiton does not exist. By marking `index.html` as the SPA you create this rule: +By default, requests for typical SPA paths (e.g. `/user/1`, `/login`) return `404 Not Found` as a file at that location does not exist. By marking `index.html` as the SPA you create this rule: *If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* @@ -72,9 +73,9 @@ Imagine the network is down or you're working offline, proxied requests to `http ```js const users = [ - { "id": 1, "name": "Lloyd", "age": 40 }, - { "id": 2, "name": "Mona", "age": 34 }, - { "id": 3, "name": "Francesco", "age": 24 } + { id: 1, name: 'Lloyd', age: 40 }, + { id: 2, name: 'Mona', age: 34 }, + { id: 3, name: 'Francesco', age: 24 } ] module.exports = MockBase => class MockUsers extends MockBase { From 923c7ac2fc2a0115c77a40f6623b46787bea67e5 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 11:50:17 +0100 Subject: [PATCH 130/136] readme --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 75b4e1b..f51158c 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,62 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 ### Mock responses -Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, Mock Responses can fill the gap. Export your mock responses from a module. +Imagine the network is down or you're working offline, proxied requests to `https://internal-service.local/api/users/1` would fail. In this case, Mock Responses can fill the gap. Mocks are defined in a module which can be reused between projects. + +Trivial example - respond to a request for `/rivers` with some JSON. Save the following Javascript in a file named `example-mocks.js`. + +```js +module.exports = MockBase => class MockRivers extends MockBase { + mocks () { + return { + route: '/rivers', + responses: [ + { + response: { type: 'json', body: [ + { name: 'Volga', drainsInto: 'Caspian Sea' }, + { name: 'Danube', drainsInto: 'Black Sea' }, + { name: 'Ural', drainsInto: 'Caspian Sea' }, + { name: 'Dnieper', drainsInto: 'Black Sea' } + ]} + } + ] + } + } +} +``` + +Launch `ws` passing in your mocks module. + +```sh +$ ws --mocks example-mocks.js +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 +``` + +GET your rivers. + +```sh +$ curl http://127.0.0.1:8000/rivers +[ + { + "name": "Volga", + "drainsInto": "Caspian Sea" + }, + { + "name": "Danube", + "drainsInto": "Black Sea" + }, + { + "name": "Ural", + "drainsInto": "Caspian Sea" + }, + { + "name": "Dnieper", + "drainsInto": "Black Sea" + } +] +``` + +More detail can be added to mocks. This example, a RESTful `/users` API, adds responses handling `PUT`, `DELETE` and `POST`. ```js const users = [ @@ -91,7 +146,7 @@ module.exports = MockBase => class MockUsers extends MockBase { { /* for GET requests return the collection */ request: { method: 'GET' }, - response: { type: 'application/json', body: users } + response: { type: 'json', body: users } }, { /* for POST requests, create a new user and return its location */ @@ -111,14 +166,14 @@ module.exports = MockBase => class MockUsers extends MockBase { } ``` -Next, launch `ws` passing in your mocks module: +Launch `ws` passing in your mocks module: ```sh $ ws --mocks example-mocks.js Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Test your mock responses. A `POST` request should return a `201` with a `Location` header and empty body. +Test your mock responses. A `POST` request should return a `201` with an empty body and the `Location` of the new resource. ```sh $ curl http://127.0.0.1:8000/users -H 'Content-type: application/json' -d '{ "name": "Anthony" }' -i @@ -129,6 +184,8 @@ Content-Type: text/plain; charset=utf-8 Content-Length: 7 Date: Wed, 28 Jun 2017 20:31:19 GMT Connection: keep-alive + +Created ``` A `GET` to `/users` should return our mock user data, including the record just added. From b583b6c56bae1864f6b7c22fca7c6d9340b91b7b Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 12:04:28 +0100 Subject: [PATCH 131/136] readme --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f51158c..a4f6165 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,15 @@ module.exports = MockBase => class MockRivers extends MockBase { route: '/rivers', responses: [ { - response: { type: 'json', body: [ - { name: 'Volga', drainsInto: 'Caspian Sea' }, - { name: 'Danube', drainsInto: 'Black Sea' }, - { name: 'Ural', drainsInto: 'Caspian Sea' }, - { name: 'Dnieper', drainsInto: 'Black Sea' } - ]} + response: { + type: 'json', + body: [ + { name: 'Volga', drainsInto: 'Caspian Sea' }, + { name: 'Danube', drainsInto: 'Black Sea' }, + { name: 'Ural', drainsInto: 'Caspian Sea' }, + { name: 'Dnieper', drainsInto: 'Black Sea' } + ] + } } ] } From 4533b5c1d4961b6588c2e43ff45e195978e61178 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 16:30:57 +0100 Subject: [PATCH 132/136] docs --- README.md | 2 +- lib/cli-app.js | 3 --- lib/command/serve.js | 4 ---- lib/local-web-server.js | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a4f6165..cf7cf56 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # local-web-server -The modular web server for productive full-stack development. +The modular web server for productive full-stack development, powered by [lws](https://github.com/lwsjs/lws). Use this tool to: diff --git a/lib/cli-app.js b/lib/cli-app.js index da2b862..33a17fd 100644 --- a/lib/cli-app.js +++ b/lib/cli-app.js @@ -1,9 +1,6 @@ 'use strict' const LwsCliApp = require('lws/lib/cli-app') -/** - * @alias module:local-web-server - */ class WsCliApp extends LwsCliApp { constructor (options) { super (options) diff --git a/lib/command/serve.js b/lib/command/serve.js index d5a0aa9..8845a62 100644 --- a/lib/command/serve.js +++ b/lib/command/serve.js @@ -1,10 +1,6 @@ const ServeCommand = require('lws/lib/command/serve') const path = require('path') -/** - * @module local-web-server - */ - class WsServe extends ServeCommand { execute (options, argv) { const usage = require('lws/lib/usage') diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 5455322..e6c9e76 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,7 +1,41 @@ const Lws = require('lws') const path = require('path') +/** + * @module local-web-server + * @example + * const LocalWebServer = require('local-web-server') + * const localWebServer = new LocalWebServer() + * const server = localWebServer.create({ + * port: port, + * directory: 'src' + * }) + */ + + /** + * @alias module:local-web-server + */ class LocalWebServer extends Lws { + /** + * Create a listening HTTP/HTTPS server. + * @param [options] {object} - Server options + * @param [options.port] {number} - Port + * @param [options.hostname] {string} -The hostname (or IP address) to listen on. Defaults to 0.0.0.0. + * @param [options.maxConnections] {number} - The maximum number of concurrent connections supported by the server. + * @param [options.keepAliveTimeout] {number} - The period (in milliseconds) of inactivity a connection will remain open before being destroyed. Set to `0` to keep connections open indefinitely. + * @param [options.configFile] {string} - Config file path, defaults to 'lws.config.js'. + * @param [options.https] {boolean} - Enable HTTPS using a built-in key and cert registered to the domain 127.0.0.1. + * @param [options.key] {string} - SSL key file path. Supply along with --cert to launch a https server. + * @param [options.cert] {string} - SSL cert file path. Supply along with --key to launch a https server. + * @param [options.pfx] {string} - Path to an PFX or PKCS12 encoded private key and certificate chain. An alternative to providing --key and --cert. + * @param [options.ciphers] {string} - Optional cipher suite specification, replacing the default. + * @param [options.secureProtocol] {string} - Optional SSL method to use, default is "SSLv23_method". + * @param [options.stack] {string[]|Middlewares[]} - Array of feature classes, or filenames of modules exporting a feature class. + * @param [options.server] {string|ServerFactory} - Custom server factory, e.g. lws-http2. + * @param [options.websocket] {string|Websocket} - Path to a websocket module + * @param [options.moduleDir] {string[]} - One or more directories to search for feature modules. + * @returns {Server} + */ create (options) { const usage = require('lws/lib/usage') usage.defaults From 6757521946cbff44fbf14d63ec45625a45569d61 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 20:01:56 +0100 Subject: [PATCH 133/136] rename localWebServer.create to listen --- lib/local-web-server.js | 6 +++--- package.json | 6 +++--- test/test.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/local-web-server.js b/lib/local-web-server.js index e6c9e76..dfd92f1 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -6,7 +6,7 @@ const path = require('path') * @example * const LocalWebServer = require('local-web-server') * const localWebServer = new LocalWebServer() - * const server = localWebServer.create({ + * const server = localWebServer.listen({ * port: port, * directory: 'src' * }) @@ -36,7 +36,7 @@ class LocalWebServer extends Lws { * @param [options.moduleDir] {string[]} - One or more directories to search for feature modules. * @returns {Server} */ - create (options) { + listen (options) { const usage = require('lws/lib/usage') usage.defaults .set('an', 'ws') @@ -47,7 +47,7 @@ class LocalWebServer extends Lws { modulePrefix: 'lws-', stack: require('./default-stack') }, options) - return super.create(options) + return super.listen(options) } } diff --git a/package.json b/package.json index 9cec93e..6b9ddfd 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,9 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre3.1", + "lws": "^1.0.0-pre4.0", "lws-blacklist": "^0.2.1", - "lws-body-parser": "^0.2.2", + "lws-body-parser": "^0.2.4", "lws-compress": "^0.2.1", "lws-conditional-get": "^0.3.3", "lws-cors": "^0.3.2", @@ -46,7 +46,7 @@ "lws-request-monitor": "^0.1.3", "lws-rewrite": "^0.3.5", "lws-spa": "^0.2.2", - "lws-static": "^0.4.0" + "lws-static": "^0.4.1" }, "devDependencies": { "coveralls": "^2.13.1", diff --git a/test/test.js b/test/test.js index 88427e0..7067b80 100644 --- a/test/test.js +++ b/test/test.js @@ -11,7 +11,7 @@ const runner = new TestRunner() runner.test('basic', async function () { const port = 9000 + this.index const localWebServer = new LocalWebServer() - const server = localWebServer.create({ + const server = localWebServer.listen({ port: port, directory: 'test/fixture' }) From 5da7ee12c65eed2e140373603b3e92437877ad07 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sat, 8 Jul 2017 20:02:21 +0100 Subject: [PATCH 134/136] 2.0.0-pre4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b9ddfd..f55f117 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "local-web-server", - "version": "2.0.0-pre3.0", + "version": "2.0.0-pre4.0", "description": "The modular web server for productive full-stack development", "bin": { "ws": "./bin/cli.js" From 34373217ec51c884bb7487f465279eb38d6585fd Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 9 Jul 2017 09:10:51 +0100 Subject: [PATCH 135/136] readme, docs, deps --- README.md | 4 +++- lib/local-web-server.js | 12 ++++++++---- package.json | 18 +++++++++--------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cf7cf56..dd38e05 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:80 By default, requests for typical SPA paths (e.g. `/user/1`, `/login`) return `404 Not Found` as a file at that location does not exist. By marking `index.html` as the SPA you create this rule: -*If a static file at the requested path exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* +*If a static file is requested (e.g. `/css/style.css`) then serve it, if not (e.g. `/login`) then serve the specified SPA and handle the route client-side.* + +[Read more](https://github.com/lwsjs/local-web-server/wiki/How-to-serve-a-Single-Page-Application-(SPA)). ### URL rewriting and proxied requests diff --git a/lib/local-web-server.js b/lib/local-web-server.js index dfd92f1..e549291 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -7,9 +7,13 @@ const path = require('path') * const LocalWebServer = require('local-web-server') * const localWebServer = new LocalWebServer() * const server = localWebServer.listen({ - * port: port, - * directory: 'src' + * port: 8050, + * https: true, + * directory: 'src', + * spa: 'index.html', + * websocket: 'src/websocket-server.js' * }) + * // secure, SPA server with listening websocket now ready on port 8050 */ /** @@ -17,7 +21,7 @@ const path = require('path') */ class LocalWebServer extends Lws { /** - * Create a listening HTTP/HTTPS server. + * Returns a listening HTTP/HTTPS server. * @param [options] {object} - Server options * @param [options.port] {number} - Port * @param [options.hostname] {string} -The hostname (or IP address) to listen on. Defaults to 0.0.0.0. @@ -33,7 +37,7 @@ class LocalWebServer extends Lws { * @param [options.stack] {string[]|Middlewares[]} - Array of feature classes, or filenames of modules exporting a feature class. * @param [options.server] {string|ServerFactory} - Custom server factory, e.g. lws-http2. * @param [options.websocket] {string|Websocket} - Path to a websocket module - * @param [options.moduleDir] {string[]} - One or more directories to search for feature modules. + * @param [options.moduleDir] {string[]} - One or more directories to search for modules. * @returns {Server} */ listen (options) { diff --git a/package.json b/package.json index f55f117..59658cf 100644 --- a/package.json +++ b/package.json @@ -33,19 +33,19 @@ "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { "lws": "^1.0.0-pre4.0", - "lws-blacklist": "^0.2.1", + "lws-blacklist": "^0.2.3", "lws-body-parser": "^0.2.4", "lws-compress": "^0.2.1", "lws-conditional-get": "^0.3.3", - "lws-cors": "^0.3.2", - "lws-index": "^0.3.2", + "lws-cors": "^0.3.4", + "lws-index": "^0.3.3", "lws-json": "^0.3.2", - "lws-log": "^0.3.1", - "lws-mime": "^0.2.1", - "lws-mock-response": "^0.4.0", - "lws-request-monitor": "^0.1.3", - "lws-rewrite": "^0.3.5", - "lws-spa": "^0.2.2", + "lws-log": "^0.3.2", + "lws-mime": "^0.2.2", + "lws-mock-response": "^0.4.2", + "lws-request-monitor": "^0.1.4", + "lws-rewrite": "^0.3.6", + "lws-spa": "^0.2.3", "lws-static": "^0.4.1" }, "devDependencies": { From 33270b817d4259c4ed09aef95938380db94b5ce7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Sun, 9 Jul 2017 22:57:35 +0100 Subject: [PATCH 136/136] readme, deps --- README.md | 2 ++ package.json | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dd38e05..093d5e7 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,8 @@ $ curl http://127.0.0.1:8000/users } ``` +See [the tutorials](https://github.com/lwsjs/local-web-server/wiki#tutorials) for more information and examples about mock responses. + ### HTTPS Launching a secure server is as simple as setting the `--https` flag. [See the wiki](https://github.com/lwsjs/local-web-server/wiki) for further configuration options and a guide on how to get the "green padlock" in your browser. diff --git a/package.json b/package.json index 59658cf..c02aa47 100644 --- a/package.json +++ b/package.json @@ -32,18 +32,18 @@ "repository": "https://github.com/lwsjs/local-web-server", "author": "Lloyd Brookes <75pound@gmail.com>", "dependencies": { - "lws": "^1.0.0-pre4.0", + "lws": "^1.0.0", "lws-blacklist": "^0.2.3", "lws-body-parser": "^0.2.4", "lws-compress": "^0.2.1", "lws-conditional-get": "^0.3.3", - "lws-cors": "^0.3.4", + "lws-cors": "^0.3.5", "lws-index": "^0.3.3", "lws-json": "^0.3.2", "lws-log": "^0.3.2", "lws-mime": "^0.2.2", - "lws-mock-response": "^0.4.2", - "lws-request-monitor": "^0.1.4", + "lws-mock-response": "^0.4.5", + "lws-request-monitor": "^0.1.5", "lws-rewrite": "^0.3.6", "lws-spa": "^0.2.3", "lws-static": "^0.4.1"