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/.travis.yml b/.travis.yml index e4ae8e8..343e041 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,4 @@ language: node_js node_js: - - 4 - - 5 - - 6 - 7 + - 8 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 29f4c67..093d5e7 100644 --- a/README.md +++ b/README.md @@ -1,647 +1,246 @@ -[![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) +[![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) [![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) -***This project does not yet use the latest Koa modules (therefore some dependencies are out of date) because the recent Koa upgrade made node v7.6 the minimum supported version. This tool supports node v4 and higher. The next version of this tool is in progress and [available for preview](https://github.com/lwsjs/local-web-server/tree/next).*** +**This documentation is a work in progress** # local-web-server -A simple web-server for productive front-end development. Typical use cases: - -* 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) - * Bundle with your front-end project - * Very little configuration, just a few options - * Outputs a dynamic statistics view to the terminal - * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/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). - * 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`. - -
$ ws --help
-
-local-web-server
-
-  A simple web-server for productive front-end development.
-
-Synopsis
-
-  $ ws [<server options>]
-  $ ws --config
-  $ ws --help
+The modular web server for productive full-stack development, powered by [lws](https://github.com/lwsjs/lws).
 
-Server
+Use this tool to:
 
-  -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.
+* 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
 
-Misc
+Features:
 
-  -h, --help    Print these usage instructions.
-  --config      Print the stored config.
+* 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
+* Single Page Application support
+* Response mocking
+* Configurable access log
+* Route blacklisting
+* HTTP Conditional Request support
+* Gzip response compression and much more
 
-  Project home: https://github.com/lwsjs/local-web-server
-
- -## Examples - -For the examples below, we assume we're in a project directory looking like this: +## Synopsis -```sh -. -├── css -│   └── style.css -├── index.html -└── package.json -``` +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)). -**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 web site -### Static 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. -Fire up your static site on the default port: ```sh $ ws -serving at http://localhost:8000 +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -[Example](https://github.com/lwsjs/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/lwsjs/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`): +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 --rewrite '/css/:stylesheet -> /build/css/:stylesheet' +$ ws --spa index.html +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -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' -``` +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: -this rewrites `/css/a` as `/build/css/a`, `/css/a/b/c` as `/build/css/a/b/c` etc. +*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.* -#### Proxied requests +[Read more](https://github.com/lwsjs/local-web-server/wiki/How-to-serve-a-Single-Page-Application-(SPA)). -If the `to` URL contains a remote host, local-web-server will act as a proxy - fetching and responding with the remote resource. +### URL rewriting and proxied requests -Mount the npm registry locally: -```sh -$ ws --rewrite '/npm/* -> http://registry.npmjs.org/$1' -``` +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/`: -Map local requests for repo data to the Github API: ```sh -$ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name' +$ 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 ``` -[Example](https://github.com/lwsjs/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": "" - } - } - ] -} -``` +### Mock responses -#### Multiple Potential 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. Mocks are defined in a module which can be reused between projects. -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: +Trivial example - respond to a request for `/rivers` with some JSON. Save the following Javascript in a file named `example-mocks.js`. -```json -{ - "mocks": [ - { - "route": "/three", - "responses": [ - { - "request": { "method": "GET" }, - "response": { - "body": "

Mock response for 'GET' request on /three

" - } - }, +```js +module.exports = MockBase => class MockRivers extends MockBase { + mocks () { + return { + route: '/rivers', + responses: [ { - "request": { "method": "POST" }, - "response": { - "status": 400, - "body": { "message": "That method is not allowed." } + response: { + type: 'json', + body: [ + { name: 'Volga', drainsInto: 'Caspian Sea' }, + { name: 'Danube', drainsInto: 'Black Sea' }, + { name: 'Ural', drainsInto: 'Caspian Sea' }, + { name: 'Dnieper', drainsInto: 'Black Sea' } + ] } } ] } - ] -} -``` - -#### 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. +Launch `ws` passing in your mocks module. -```json -{ - "mocks": [ - { "route": "/users", "module": "/mocks/users.js" }, - { "route": "/users/:id", "module": "/mocks/user.js" } - ] -} +```sh +$ ws --mocks example-mocks.js +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -Define a module (`users.json`) defining seed data: +GET your rivers. -```json +```sh +$ curl http://127.0.0.1:8000/rivers [ - { "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 - }) - } + "name": "Volga", + "drainsInto": "Caspian Sea" }, { - /* 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)) - } + "name": "Danube", + "drainsInto": "Black Sea" }, - - /* 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 - } + "name": "Ural", + "drainsInto": "Caspian Sea" }, - - /* 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 - } + "name": "Dnieper", + "drainsInto": "Black Sea" } ] - -module.exports = mockResponses -``` - -[Example](https://github.com/lwsjs/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` +More detail can be added to mocks. This example, a RESTful `/users` API, adds responses handling `PUT`, `DELETE` and `POST`. -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. - -### Stored config +```js +const users = [ + { id: 1, name: 'Lloyd', age: 40 }, + { id: 2, name: 'Mona', age: 34 }, + { id: 3, name: 'Francesco', age: 24 } +] -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" +module.exports = MockBase => class MockUsers extends MockBase { + mocks () { + /* response mocks for /users */ + return [ + { + 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: '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}`) + } + } + ] + } + ] } } ``` -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. +Launch `ws` passing in your mocks module: -To inspect stored config, run: ```sh -$ ws --config +$ ws --mocks example-mocks.js +Serving at http://mbp.local:8000, http://127.0.0.1:8000, http://192.168.0.100:8000 ``` -### Logging -By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log-format`: +Test your mock responses. A `POST` request should return a `201` with an empty body and the `Location` of the new resource. ```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 -``` +$ 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 -[Example](https://github.com/lwsjs/local-web-server/tree/master/example/forbid). - -### Other usage - -#### Debugging - -Prints information about loaded middleware, arguments, remote proxy fetches etc. -```sh -$ ws --verbose +Created ``` -#### Compression +A `GET` to `/users` should return our mock user data, including the record just added. -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 -``` - -#### 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" ] +$ curl http://127.0.0.1:8000/users +[ + { + "id": 1, + "name": "Lloyd", + "age": 40 + }, + { + "id": 2, + "name": "Mona", + "age": 34 + }, + { + "id": 3, + "name": "Francesco", + "age": 24 + }, + { + "id": 4, + "name": "Anthony" } -} ``` -[Example](https://github.com/lwsjs/local-web-server/tree/master/example/mime-override). +See [the tutorials](https://github.com/lwsjs/local-web-server/wiki#tutorials) for more information and examples about mock responses. -#### Log Visualisation -Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/lwsjs/local-web-server/blob/master/doc/visualisation.md). +### HTTPS -## Install -Ensure [node.js](http://nodejs.org) is installed first. Linux/Mac users may need to run the following commands with `sudo`. +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 -$ npm install -g local-web-server +$ ws --https +Serving at https://mbp.local:8000, https://127.0.0.1:8000, https://192.168.0.100:8000 ``` -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 -``` +## Further Documentation -2\. Add a `start` command to your `package.json`: +[See the wiki for plenty more documentation and tutorials](https://github.com/lwsjs/local-web-server/wiki). -```json -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100, - "forbid": "*.json" - }, - "scripts": { - "start": "ws" - } -} -``` +## Install -3\. Document how to build and launch your site +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 -$ npm start -serving at http://localhost:8100 +$ npm install -g local-web-server@next ``` - -## 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" } - ] -} -``` - * * * -© 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 c44161b..c108e2e 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,158 +1,2 @@ #!/usr/bin/env node -'use strict' -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') -const os = require('os') -const arrayify = require('array-back') -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 - -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 */ - return - } - - const app = 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.on('error', err => { - if (options.server['log-format']) { - console.error(ansi.format(err.message, 'red')) - } - }) - - 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') - isHttps = true - - 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) - } else { - app.listen(options.server.port, onServerUp) - } -} - -function stop (msgs, exitCode) { - arrayify(msgs).forEach(msg => console.error(ansi.format(msg))) - 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 - .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 collectOptions () { - let options = {} - - /* parse command line args */ - options = commandLineArgs(cliOptions.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) { - 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 -} +require('../lib/cli-app').run() diff --git a/doc/img/logstagia.gif b/doc/img/logstagia.gif deleted file mode 100644 index 22415e0..0000000 Binary files a/doc/img/logstagia.gif and /dev/null differ diff --git a/doc/visualisation.md b/doc/visualisation.md deleted file mode 100644 index b9b04cb..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 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/example/forbid/.local-web-server.json b/example/forbid/.local-web-server.json deleted file mode 100644 index dd8124a..0000000 --- a/example/forbid/.local-web-server.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "forbid": [ - "/admin/*", "*.php" - ] -} diff --git a/example/forbid/admin/blocked.html b/example/forbid/admin/blocked.html deleted file mode 100644 index f51dbee..0000000 --- a/example/forbid/admin/blocked.html +++ /dev/null @@ -1 +0,0 @@ -

Forbidden page

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

A permitted page

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

You waited 2s for this

' - resolve() - }, 2000) - }) - } -} diff --git a/example/mock/.local-web-server.json b/example/mock/.local-web-server.json deleted file mode 100644 index 736460e..0000000 --- a/example/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": "/four", - "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/mock/mocks/five.js b/example/mock/mocks/five.js deleted file mode 100644 index 091186b..0000000 --- a/example/mock/mocks/five.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - 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 deleted file mode 100644 index 981fe26..0000000 --- a/example/mock/mocks/stream-self.js +++ /dev/null @@ -1,7 +0,0 @@ -const fs = require('fs') - -module.exports = { - response: { - body: fs.createReadStream(__filename) - } -} diff --git a/example/mock/mocks/user.js b/example/mock/mocks/user.js deleted file mode 100644 index 1998936..0000000 --- a/example/mock/mocks/user.js +++ /dev/null @@ -1,38 +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 */ - { - 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 diff --git a/example/mock/mocks/users.js b/example/mock/mocks/users.js deleted file mode 100644 index 8e845f2..0000000 --- a/example/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/mock/mocks/users.json b/example/mock/mocks/users.json deleted file mode 100644 index 09a166b..0000000 --- a/example/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/rewrite/.local-web-server.json b/example/rewrite/.local-web-server.json deleted file mode 100644 index 1eb9003..0000000 --- a/example/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/rewrite/build/styles/style.css b/example/rewrite/build/styles/style.css deleted file mode 100644 index 0a36465..0000000 --- a/example/rewrite/build/styles/style.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - font-family: monospace; - font-size: 1.3em; -} diff --git a/example/rewrite/index.html b/example/rewrite/index.html deleted file mode 100644 index 02dcfd3..0000000 --- a/example/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/simple/css/style.css b/example/simple/css/style.css deleted file mode 100644 index 7fb71e5..0000000 --- a/example/simple/css/style.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - background-color: #AA3939; - color: #FFE2E2 -} -svg { - fill: #000 -} diff --git a/example/simple/index.html b/example/simple/index.html deleted file mode 100644 index 008f97d..0000000 --- a/example/simple/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -

Amazing Page

-

- With a freaky triangle.. -

- - - diff --git a/example/simple/package.json b/example/simple/package.json deleted file mode 100644 index 03e697a..0000000 --- a/example/simple/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "example", - "version": "1.0.0", - "local-web-server": { - "port": 8100 - } -} diff --git a/example/spa/.local-web-server.json b/example/spa/.local-web-server.json deleted file mode 100644 index 2c63606..0000000 --- a/example/spa/.local-web-server.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "spa": "index.html" -} diff --git a/example/spa/css/style.css b/example/spa/css/style.css deleted file mode 100644 index 793042d..0000000 --- a/example/spa/css/style.css +++ /dev/null @@ -1,5 +0,0 @@ -body { - background-color: IndianRed; -} - -a { color: black } diff --git a/example/spa/index.html b/example/spa/index.html deleted file mode 100644 index 9a46a23..0000000 --- a/example/spa/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - -

Single Page App

-

Location:

- - diff --git a/jsdoc2md/README.hbs b/jsdoc2md/README.hbs deleted file mode 100644 index 4f2661b..0000000 --- a/jsdoc2md/README.hbs +++ /dev/null @@ -1,597 +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/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/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) - -***This project does not yet use the latest Koa modules (therefore some dependencies are out of date) because the recent Koa upgrade made node v7.6 the minimum supported version. This tool supports node v4 and higher. The next version of this tool is in progress and [available for preview](https://github.com/lwsjs/local-web-server/tree/next).*** - -# local-web-server -A simple web-server for productive front-end development. Typical use cases: - -* 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) - * Bundle with your front-end project - * Very little configuration, just a few options - * Outputs a dynamic statistics view to the terminal - * Configurable log output, compatible with [Goaccess, Logstalgia and glTail](https://github.com/lwsjs/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). - * 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`. - -
$ 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/lwsjs/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/lwsjs/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/lwsjs/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/lwsjs/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/lwsjs/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/lwsjs/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/lwsjs/local-web-server/tree/master/example/forbid). - -### 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 -``` - -#### 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). - -#### Log Visualisation -Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/lwsjs/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/lib/cli-app.js b/lib/cli-app.js new file mode 100644 index 0000000..33a17fd --- /dev/null +++ b/lib/cli-app.js @@ -0,0 +1,14 @@ +'use strict' +const LwsCliApp = require('lws/lib/cli-app') + +class WsCliApp extends LwsCliApp { + constructor (options) { + super (options) + /* override default serve command */ + this.commands.add(null, require('./command/serve')) + /* add middleware-list command */ + this.commands.add('middleware-list', require('./command/middleware-list')) + } +} + +module.exports = WsCliApp diff --git a/lib/cli-options.js b/lib/cli-options.js deleted file mode 100644 index 11286b4..0000000 --- a/lib/cli-options.js +++ /dev/null @@ -1,85 +0,0 @@ -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: 'Usage', - content: [ - '$ ws []', - '$ ws --config', - '$ ws --help' - ] - }, - { - header: 'Server', - optionList: exports.definitions, - group: 'server' - }, - { - header: 'Misc', - optionList: exports.definitions, - group: 'misc' - }, - { - content: 'Project home: [underline]{https://github.com/lwsjs/local-web-server}' - } -] diff --git a/lib/command/middleware-list.js b/lib/command/middleware-list.js new file mode 100644 index 0000000..557efc4 --- /dev/null +++ b/lib/command/middleware-list.js @@ -0,0 +1,11 @@ +class MiddlewareList { + description () { + return 'Print available middleware' + } + execute (options) { + const list = require('../default-stack') + console.log(list) + } +} + +module.exports = MiddlewareList diff --git a/lib/command/serve.js b/lib/command/serve.js new file mode 100644 index 0000000..8845a62 --- /dev/null +++ b/lib/command/serve.js @@ -0,0 +1,49 @@ +const ServeCommand = require('lws/lib/command/serve') +const path = require('path') + +class WsServe extends ServeCommand { + execute (options, argv) { + const usage = require('lws/lib/usage') + usage.defaults + .set('an', 'ws') + .set('av', require('../../package').version) + .set('cd4', 'cli') + options = { + stack: require('../default-stack'), + moduleDir: path.resolve(__dirname, `../../node_modules`), + modulePrefix: 'lws-' + } + return super.execute(options, argv) + } + + usage () { + const sections = super.usage() + sections.shift() + sections.shift() + sections.pop() + sections.unshift( + { + header: 'local-web-server', + content: 'The modular development web server for productive full-stack engineers.' + }, + { + 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/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 20539d3..e549291 100644 --- a/lib/local-web-server.js +++ b/lib/local-web-server.js @@ -1,224 +1,58 @@ -'use strict' +const Lws = require('lws') const path = require('path') -const url = require('url') -const arrayify = require('array-back') /** * @module local-web-server - */ -module.exports = localWebServer - -/** - * 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) + * const LocalWebServer = require('local-web-server') + * const localWebServer = new LocalWebServer() + * const server = localWebServer.listen({ + * 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 */ -function localWebServer (options) { - options = Object.assign({ - static: {}, - serveIndex: {}, - 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' - } - - 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 Koa = require('koa') - 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') - - const app = new Koa() - const _use = app.use - app.use = x => _use.call(app, convert(x)) - - /* CORS: allow from any origin */ - app.use(cors()) - - /* pretty print JSON */ - app.use(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}`) - app.use(_.all(route.from, mw.proxyRequest(route, app))) - } else { - const rewrite = require('koa-rewrite') - const rmw = rewrite(route.from, route.to) - rmw._name = 'rewrite' - app.use(rmw) - } - } - }) - } - - /* must come after rewrite. See https://github.com/nodejitsu/node-http-proxy/issues/180. */ - app.use(bodyParser()) - - /* path blacklist */ - if (options.forbid.length) { - debug('forbid', options.forbid.join(', ')) - app.use(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()) - } - - /* mime-type overrides */ - if (options.mime) { - debug('mime override', JSON.stringify(options.mime)) - app.use(mw.mime(options.mime)) - } - /* compress response */ - if (options.compress) { - const compress = require('koa-compress') - debug('compression', 'enabled') - app.use(compress()) + /** + * @alias module:local-web-server + */ +class LocalWebServer extends Lws { + /** + * 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. + * @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 modules. + * @returns {Server} + */ + listen (options) { + const usage = require('lws/lib/usage') + usage.defaults + .set('an', 'ws') + .set('av', require('../package').version) + .set('cd4', 'api') + options = Object.assign({ + moduleDir: path.resolve(__dirname, `../node_modules`), + modulePrefix: 'lws-', + stack: require('./default-stack') + }, options) + return super.listen(options) } - - /* 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('common', log.options)) - } else if (log.format === 'logstalgia') { - morgan.token('date', logstalgiaDate) - app.use(morgan('combined', log.options)) - } else { - app.use(morgan(log.format, log.options)) - } - } - - /* Mock Responses */ - options.mocks.forEach(mock => { - if (mock.module) { - mock.responses = require(path.resolve(path.join(options.static.root, mock.module))) - } - - if (mock.responses) { - app.use(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)) - } - }) - - /* 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) - app.use(historyApiFallback({ - index: options.spa, - verbose: options.verbose - })) - } - - /* serve static files */ - if (options.static.root) { - const serve = require('koa-static') - app.use(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)) - } - - return app -} - -function logstalgiaDate () { - var d = new Date() - return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '') } -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 - */ - -/** - * @external KoaApplication - * @see https://github.com/koajs/koa/blob/master/docs/api/index.md#application - */ +module.exports = LocalWebServer diff --git a/lib/middleware.js b/lib/middleware.js deleted file mode 100644 index b6ec3fd..0000000 --- a/lib/middleware.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict' -const path = require('path') -const http = require('http') -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') - -/** - * @module middleware - */ -exports.proxyRequest = proxyRequest -exports.blacklist = blacklist -exports.mockResponses = mockResponses -exports.mime = mime - -function proxyRequest (route) { - const httpProxy = require('http-proxy') - const proxy = httpProxy.createProxyServer({ - changeOrigin: true, - secure: false - }) - proxy.on('error', err => { - // not worth crashing for - }) - - 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 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(() => { - 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 - }) - }) - } -} - -function mockResponses (route, targets) { - targets = arrayify(targets) - debug('mock route: %s, targets: %s', route, targets.length) - const pathRe = pathToRegexp(route) - - 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) - } - - 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 9d7740d..c02aa47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "local-web-server", - "version": "1.2.8", - "description": "A simple web-server for productive front-end development", + "version": "2.0.0-pre4.0", + "description": "The modular web server for productive full-stack development", "bin": { "ws": "./bin/cli.js" }, @@ -16,50 +16,41 @@ "development", "cors", "mime", - "rest" + "rest", + "mock", + "api", + "proxy" ], "engines": { - "node": ">=4.0.0" + "node": ">=7.6" }, "scripts": { - "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" + "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/test-runner test/*.js && cat coverage/lcov.info | coveralls" }, "repository": "https://github.com/lwsjs/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.2", - "command-line-usage": "^4.0.0", - "config-master": "^3.0.0", - "debug": "^2.2.0", - "http-proxy": "^1.15.1", - "kcors": "^1.3.0", - "koa": "2.0.1", - "koa-bodyparser": "^3.0.0", - "koa-compress": "^1.0.9", - "koa-conditional-get": "^1.0.3", - "koa-connect-history-api-fallback": "^0.3.1", - "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-send": "^3.2.0", - "koa-serve-index": "^1.1.1", - "koa-static": "^2.0.0", - "path-to-regexp": "^1.6.0", - "reduce-flatten": "^1.0.1", - "stream-log-stats": "^2.0.2", - "test-value": "^2.1.0", - "typical": "^2.6.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.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.5", + "lws-request-monitor": "^0.1.5", + "lws-rewrite": "^0.3.6", + "lws-spa": "^0.2.3", + "lws-static": "^0.4.1" }, "devDependencies": { - "jsdoc-to-markdown": "^3.0.0", - "req-then": "^0.2.4", - "tape": "^4.6.2" + "coveralls": "^2.13.1", + "req-then": "^0.6.4", + "test-runner": "^0.4.0" } } 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----- 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 +}) 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/big-file.txt b/test/fixture/big-file.txt
deleted file mode 100644
index dc02cef..0000000
--- a/test/fixture/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/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)
-
-# 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/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/forbid/one.html b/test/fixture/forbid/one.html
deleted file mode 100644
index 5626abf..0000000
--- a/test/fixture/forbid/one.html
+++ /dev/null
@@ -1 +0,0 @@
-one
diff --git a/test/fixture/forbid/two.php b/test/fixture/forbid/two.php
deleted file mode 100644
index abb2fca..0000000
--- a/test/fixture/forbid/two.php
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/test/fixture/spa/one.txt b/test/fixture/one.txt
similarity index 100%
rename from test/fixture/spa/one.txt
rename to test/fixture/one.txt
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/rewrite/one.html b/test/fixture/rewrite/one.html
deleted file mode 100644
index 5626abf..0000000
--- a/test/fixture/rewrite/one.html
+++ /dev/null
@@ -1 +0,0 @@
-one
diff --git a/test/fixture/something.php b/test/fixture/something.php
deleted file mode 100644
index abb2fca..0000000
--- a/test/fixture/something.php
+++ /dev/null
@@ -1 +0,0 @@
-
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
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()
-      })
-    })
-  })
-})
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/test.js b/test/test.js
index 838605d..7067b80 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1,305 +1,21 @@
 'use strict'
-const test = require('tape')
+const TestRunner = require('test-runner')
 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))
-  })
+const LocalWebServer = require('../')
+const a = require('assert')
+const usage = require('lws/lib/usage')
+usage.disable()
+
+const runner = new TestRunner()
+
+runner.test('basic', async function () {
+  const port = 9000 + this.index
+  const localWebServer = new LocalWebServer()
+  const server = localWebServer.listen({
+    port: port,
+    directory: 'test/fixture'
+  })
+  const response = await request(`http://localhost:${port}/one.txt`)
+  server.close()
+  a.strictEqual(response.data.toString(), 'one\n')
 })