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