Lloyd Brookes
9 years ago
13 changed files with 817 additions and 1114 deletions
-
446README.md
-
9doc/blacklist.md
-
59doc/https.md
-
69doc/logging.md
-
12doc/mime-types.md
-
241doc/mock-response.md
-
37doc/rewrite.md
-
12doc/spa.md
-
28doc/stored-config.md
-
416jsdoc2md/README.hbs
-
291lib/default-stack.js
-
17lib/local-web-server.js
-
294lib/middleware-stack.js
@ -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). |
@ -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. |
@ -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 |
|||
``` |
@ -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). |
@ -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 <code>[route](http://expressjs.com/guide/routing.html#route-paths)</code> to a `response`. A simple home page: |
|||
```json |
|||
{ |
|||
"mocks": [ |
|||
{ |
|||
"route": "/", |
|||
"response": { |
|||
"body": "<h1>Welcome to the Mock Responses example</h1>" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
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": "<h1>Welcome to the Mock Responses example</h1>" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
### 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": "<result id='2' name='whatever' />" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
``` |
|||
|
|||
### 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": "<h1>Mock response for 'GET' request on /three</h1>" |
|||
} |
|||
}, |
|||
{ |
|||
"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 = '<h1>I can do anything i want.</h1>' |
|||
} |
|||
} |
|||
``` |
|||
|
|||
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 = `<h1>id: ${id}, name: ${ctx.query.name}</h1>` |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 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). |
@ -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). |
@ -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). |
@ -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 |
|||
``` |
@ -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 |
|||
} |
|||
}) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue