diff --git a/README.md b/README.md index ff4b2de..2739ff1 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ $ ws --spa index.html By default, typical SPA urls (e.g. `/user/1`, `/login`) would return `404 Not Found` as there is no file at that location on disk. By marking `index.html` as the SPA you create this rule: -*if a file with that url exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then pass it to the SPA.* +*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 SPA.* ### Access Control -Access to all files is allowed, beside those you forbid (e.g. config files): +Access to all files is allowed, beside those in the forbidden list (e.g. config files): ```sh $ ws --forbid .json .yml serving at http://localhost:8000 @@ -54,6 +54,8 @@ When urls don't map to your directory structure, rewrite: $ ws --rewrite /css=>/build/css ``` +### Proxy + Rewrite to remote servers (proxy): ```sh $ ws --rewrite "/api => http://api.example.com/api" \ @@ -61,9 +63,6 @@ $ ws --rewrite "/api => http://api.example.com/api" \ "/user/:project/repo -> https://api.github.com/repos/:project" ``` -### Mock Responses -*Coming soon*. - ### Stored config Always use this port and blacklist? Persist it to the config: @@ -87,6 +86,9 @@ 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" ``` +### Mock Responses +*Coming soon*. + ### Other features Compression, caching, simple statistics view, log, override mime types. @@ -208,6 +210,7 @@ Returns a Koa application **Example** ```js const localWebServer = require('local-web-server') +localWebServer().listen(8000) ``` ## Composition diff --git a/bin/cli.js b/bin/cli.js index 411f91c..942d02a 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -43,7 +43,8 @@ localWebServer({ forbid: options.server.forbid.map(regexp => RegExp(regexp, "i")), proxyRoutes: options.server.proxyRoutes, spa: options.server.spa, - 'no-cache': options.server['no-cache'] + 'no-cache': options.server['no-cache'], + rewrite: options.server.rewrite }).listen(options.server.port, onServerUp) function halt (err) { @@ -74,10 +75,9 @@ function collectOptions () { const builtIn = { port: 8000, - root: process.cwd(), // root dir when using multiple static dirs directory: process.cwd(), - proxyRoutes: [], - forbid: [] + forbid: [], + proxyRoutes: [] } /* override built-in defaults with stored config and then command line args */ diff --git a/example/rewrite/.local-web-server.json b/example/rewrite/.local-web-server.json new file mode 100644 index 0000000..bcf0623 --- /dev/null +++ b/example/rewrite/.local-web-server.json @@ -0,0 +1,5 @@ +{ + "rewrite": [ + { "from": "/css/*", "to": "/styles/$1" } + ] +} diff --git a/example/rewrite/index.html b/example/rewrite/index.html new file mode 100644 index 0000000..008f97d --- /dev/null +++ b/example/rewrite/index.html @@ -0,0 +1,10 @@ + + + +

Amazing Page

+

+ With a freaky triangle.. +

+ + + diff --git a/example/rewrite/styles/style.css b/example/rewrite/styles/style.css new file mode 100644 index 0000000..7fb71e5 --- /dev/null +++ b/example/rewrite/styles/style.css @@ -0,0 +1,7 @@ +body { + background-color: #AA3939; + color: #FFE2E2 +} +svg { + fill: #000 +} diff --git a/jsdoc2md/README.hbs b/jsdoc2md/README.hbs index 8daa6d3..5fd2e45 100644 --- a/jsdoc2md/README.hbs +++ b/jsdoc2md/README.hbs @@ -37,11 +37,11 @@ $ ws --spa index.html By default, typical SPA urls (e.g. `/user/1`, `/login`) would return `404 Not Found` as there is no file at that location on disk. By marking `index.html` as the SPA you create this rule: -*if a file with that url exists (e.g. `/css/style.css`) then serve it, if it does not (e.g. `/login`) then pass it to the SPA.* +*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 SPA.* ### Access Control -Access to all files is allowed, beside those you forbid (e.g. config files): +Access to all files is allowed, beside those in the forbidden list (e.g. config files): ```sh $ ws --forbid .json .yml serving at http://localhost:8000 @@ -54,6 +54,8 @@ When urls don't map to your directory structure, rewrite: $ ws --rewrite /css=>/build/css ``` +### Proxy + Rewrite to remote servers (proxy): ```sh $ ws --rewrite "/api => http://api.example.com/api" \ @@ -61,9 +63,6 @@ $ ws --rewrite "/api => http://api.example.com/api" \ "/user/:project/repo -> https://api.github.com/repos/:project" ``` -### Mock Responses -*Coming soon*. - ### Stored config Always use this port and blacklist? Persist it to the config: @@ -87,6 +86,9 @@ 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" ``` +### Mock Responses +*Coming soon*. + ### Other features Compression, caching, simple statistics view, log, override mime types. diff --git a/lib/cli-options.js b/lib/cli-options.js index 5ba58de..13e3775 100644 --- a/lib/cli-options.js +++ b/lib/cli-options.js @@ -5,7 +5,7 @@ module.exports = { description: 'Web server port', group: 'server' }, { - name: 'log-format', alias: 'l', type: String, + name: 'log-format', alias: 'f', type: String, description: "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').", group: 'server' }, { @@ -17,7 +17,7 @@ module.exports = { description: 'Enable gzip compression, reduces bandwidth.', group: 'server' }, { - name: 'forbid', alias: 'f', type: String, multiple: true, typeLabel: '[underline]{regexp} ...', + name: 'forbid', alias: 'b', type: String, multiple: true, typeLabel: '[underline]{regexp} ...', description: 'A list of forbidden routes', group: 'server' }, { @@ -25,6 +25,10 @@ module.exports = { description: 'Disable etag-based caching - forces loading from disk each request.', group: 'server' }, { + name: 'rewrite', alias: 'r', type: String, multiple: true, typeLabel: '[underline]{expression} ...', + description: 'A list of URL rewrite rules', group: 'server' + }, + { name: 'help', alias: 'h', type: Boolean, description: 'Print these usage instructions', group: 'misc' }, diff --git a/lib/local-web-server.js b/lib/local-web-server.js index 863ae2b..1937298 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -3,20 +3,11 @@ const path = require('path') const http = require('http') const url = require('url') const Koa = require('koa') -const serve = require('koa-static') const convert = require('koa-convert') -const serveIndex = require('koa-serve-index') -const morgan = require('koa-morgan') -const compress = require('koa-compress') -const streamLogStats = require('stream-log-stats') const cors = require('kcors') -const conditional = require('koa-conditional-get'); -const etag = require('koa-etag'); const _ = require('koa-route') const mount = require('koa-mount') -const httpProxy = require('http-proxy') const pathToRegexp = require('path-to-regexp') -const send = require('koa-send') /** * @module local-web-server @@ -31,6 +22,7 @@ module.exports = localWebServer * @alias module:local-web-server * @example * const localWebServer = require('local-web-server') + * localWebServer().listen(8000) */ function localWebServer (options) { options = Object.assign({ @@ -40,7 +32,8 @@ function localWebServer (options) { compress: false, forbid: [], directories: [], - proxyRoutes: [] + proxyRoutes: [], + rewrite: [] }, options) const log = options.log @@ -50,33 +43,44 @@ function localWebServer (options) { const _use = app.use app.use = x => _use.call(app, convert(x)) - const proxy = httpProxy.createProxyServer({ - changeOrigin: true - }) /* Proxy routes */ - options.proxyRoutes.forEach(route => { - app.use(_.all(route.from, function * () { - const keys = [] - route.re = pathToRegexp(route.from, keys) - route.new = route.to - - this.response = false - keys.forEach((key, index) => { - const re = { - token: RegExp('\\$\\{' + key.name + '\\}', 'g'), - index: RegExp('\\$\\{' + index + '\\}', 'g') - } - route.new = route.new - .replace(re.token, arguments[index] || '') - .replace(re.index, arguments[index] || '') - }) - proxy.once('proxyReq', function (proxyReq) { - proxyReq.path = url.parse(route.new).path; - }) - proxy.web(this.req, this.res, { target: route.new }) - })) - }) + if (options.proxyRoutes.length) { + const httpProxy = require('http-proxy') + const proxy = httpProxy.createProxyServer({ + changeOrigin: true + }) + options.proxyRoutes.forEach(route => { + app.use(_.all(route.from, function * () { + const keys = [] + route.re = pathToRegexp(route.from, keys) + route.new = route.to + + this.response = false + keys.forEach((key, index) => { + const re = { + token: RegExp('\\$\\{' + key.name + '\\}', 'g'), + index: RegExp('\\$\\{' + index + '\\}', 'g') + } + route.new = route.new + .replace(re.token, arguments[index] || '') + .replace(re.index, arguments[index] || '') + }) + proxy.once('proxyReq', function (proxyReq) { + proxyReq.path = url.parse(route.new).path; + }) + proxy.web(this.req, this.res, { target: route.new }) + })) + }) + } + + /* Rewrite rules */ + if (options.rewrite && options.rewrite.length) { + const rewrite = require('koa-rewrite') + options.rewrite.forEach(rule => { + app.use(rewrite(rule.from, rule.to)) + }) + } /* CORS: allow from any origin */ app.use(cors()) @@ -94,6 +98,8 @@ function localWebServer (options) { /* Cache */ if (!options['no-cache']) { + const conditional = require('koa-conditional-get'); + const etag = require('koa-etag'); app.use(conditional()) app.use(etag()) } @@ -113,45 +119,41 @@ function localWebServer (options) { /* compress response */ if (options.compress) { + const compress = require('koa-compress') app.use(compress()) } - /* special case log formats */ - if (log.format) { - if (log.format === 'none'){ - log.format = undefined + /* 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 }) + app.use(morgan.middleware('common', log.options)) } else if (log.format === 'logstalgia') { morgan.token('date', logstalgiaDate) - log.format = 'combined' + app.use(morgan.middleware('combined', log.options)) + } else { + app.use(morgan.middleware(log.format, log.options)) } - /* if no specific log format was requested, show log stats */ - } else { - log.format = 'common' - log.options.stream = streamLogStats({ refreshRate: 500 }) } - if (log.format) app.use(morgan.middleware(log.format, log.options)) - // options.static.root = [ - // { route: '/one', root: 'lib' }, - // { route: '/two', root: 'node_modules' } - // ] /* serve static files */ if (options.static.root) { + const serve = require('koa-static') app.use(serve(options.static.root, options.static.options)) - - // options.static.root.forEach(config => { - // app.use(mount(config.route, serve(config.root))) - // app.use(mount(config.route, serveIndex(config.root))) - // }) } /* serve directory index */ if (options.serveIndex.path) { + const serveIndex = require('koa-serve-index') app.use(serveIndex(options.serveIndex.path, options.serveIndex.options)) } /* for any URL not matched by static (e.g. `/search`), serve the SPA */ if (options.spa) { + const send = require('koa-send') app.use(_.all('*', function * () { yield send(this, options.spa, { root: process.cwd() }) }))