Lloyd Brookes
9 years ago
12 changed files with 105 additions and 436 deletions
-
23example/stack/cache-control.js
-
4lib/cli-data.js
-
12lib/debug.js
-
309lib/default-stack.js
-
109lib/local-web-server.js
-
59lib/middleware.js
-
3package.json
-
18test/fixture/ajax.html
-
1test/fixture/file.txt
-
1test/fixture/one/file.txt
-
1test/fixture/spa/one.txt
-
1test/fixture/spa/two.txt
@ -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 |
@ -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 |
|||
} |
@ -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 |
|||
} |
|||
}) |
|||
} |
@ -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 |
|||
}) |
|||
}) |
|||
} |
|||
} |
@ -1,18 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<head> |
|||
<title>Ajax test</title> |
|||
</head> |
|||
<body> |
|||
<h1>README</h1> |
|||
<h2>loaded in the "Ajax" style</h2> |
|||
<pre id="readme"></pre> |
|||
<script> |
|||
var $ = document.querySelector.bind(document); |
|||
var req = new XMLHttpRequest(); |
|||
req.open("get", "http://localhost:8000/big-file.txt", true); |
|||
req.onload = function(){ |
|||
$("#readme").textContent = this.responseText; |
|||
} |
|||
req.send() |
|||
</script> |
|||
</body> |
@ -1 +0,0 @@ |
|||
test |
@ -1 +0,0 @@ |
|||
one |
@ -1 +0,0 @@ |
|||
one |
@ -1 +0,0 @@ |
|||
two |
Write
Preview
Loading…
Cancel
Save
Reference in new issue