Browse Source

Merge branch 'next'

master
Lloyd Brookes 9 years ago
parent
commit
cd08bff6ac
  1. 1
      .coveralls.yml
  2. 1
      .gitignore
  3. 18
      .jshintrc
  4. 4
      .travis.yml
  5. 256
      README.md
  6. 99
      bin/cli.js
  7. 155
      bin/ws.js
  8. BIN
      doc/img/logstagia.gif
  9. 37
      doc/visualisation.md
  10. 5
      example/forbid/.local-web-server.json
  11. 1
      example/forbid/admin/blocked.html
  12. 1
      example/forbid/allowed.html
  13. 5
      example/forbid/index.html
  14. 0
      example/forbid/something.php
  15. 5
      example/mime-override/.local-web-server.json
  16. 1
      example/mime-override/something.php
  17. 7
      example/rewrite/.local-web-server.json
  18. 4
      example/rewrite/build/styles/style.css
  19. 22
      example/rewrite/index.html
  20. 7
      example/simple/css/style.css
  21. 10
      example/simple/index.html
  22. 7
      example/simple/package.json
  23. 3
      example/spa/.local-web-server.json
  24. 3
      example/spa/css/style.css
  25. 8
      example/spa/index.html
  26. 210
      jsdoc2md/README.hbs
  27. 82
      lib/cli-options.js
  28. 193
      lib/local-web-server.js
  29. 63
      package.json
  30. 5
      test/.local-web-server.json
  31. 4
      test/fixture/ajax.html
  32. 195
      test/fixture/big-file.txt
  33. 1
      test/fixture/file.txt
  34. 1
      test/fixture/forbid/one.html
  35. 1
      test/fixture/forbid/two.php
  36. 1
      test/fixture/one/file.txt
  37. 1
      test/fixture/rewrite/one.html
  38. 1
      test/fixture/something.php
  39. 147
      test/test.js

1
.coveralls.yml

@ -0,0 +1 @@
repo_token: w9HmlMl9558e1LpP9p62YgYutkVE9PqtN

1
.gitignore

@ -1 +1,2 @@
node_modules
tmp

18
.jshintrc

@ -1,18 +0,0 @@
{
"bitwise": true,
"camelcase": true,
"eqeqeq": true,
"globals": { "describe" : false, "it": false, "beforeEach": false },
"globalstrict": false,
"indent": 4,
"latedef": true,
"laxbreak": true,
"maxparams": 3,
"multistr": true,
"newcap": true,
"node": true,
"quotmark": "double",
"trailing": true,
"undef": true,
"unused": true
}

4
.travis.yml

@ -0,0 +1,4 @@
language: node_js
node_js:
- '5.0'
- '4.2'

256
README.md

@ -1,193 +1,231 @@
[![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)
[![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/75lb/local-web-server.svg?branch=master)](https://travis-ci.org/75lb/local-web-server)
[![Dependency Status](https://david-dm.org/75lb/local-web-server.svg)](https://david-dm.org/75lb/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).
A simple web-server for productive front-end development.
![local-web-server](http://75lb.github.io/local-web-server/ws.gif)
**Requires node v4.0.0 or higher**.
## Install
Ensure [node.js](http://nodejs.org) is installed first. Linux/Mac users may need to run the following commands with `sudo`.
## Synopsis
For the examples below, we assume we're in a project directory looking like this:
### Globally
```sh
$ npm install -g local-web-server
.
├── css
│   └── style.css
├── index.html
└── package.json
```
### Bundled with your project
```sh
$ npm install local-web-server --save-dev
```
All paths/routes are specified using [express syntax](http://expressjs.com/guide/routing.html#route-paths).
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:
### Static site
Fire up your static site on the default port:
```sh
$ npm install
$ npm install -g local-web-server
$ ws
serving at http://localhost:8000
```
to the following, server implementation and launch details abstracted away:
### Single Page Application
You're building a web app with client-side routing, so mark `index.html` as the SPA.
```sh
$ npm install
$ npm start
$ ws --spa index.html
```
## Usage
```
Usage
$ ws <server options>
$ ws --config
$ ws --help
By default, typical SPA urls (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:
Server
-p, --port <number> Web server port
-f, --log-format <string> 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 <string> Root directory, defaults to the current directory
-c, --compress Enable gzip compression, reduces bandwidth.
-r, --refresh-rate <number> Statistics view refresh rate in ms. Defaults to 500.
*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.*
Misc
-h, --help Print these usage instructions
--config Print the stored config
```
### Access Control
From the folder you wish to serve, run:
By default, access to all files is allowed (including dot files). Use `--forbid` to establish a blacklist:
```sh
$ ws
$ ws --forbid '*.json' '*.yml'
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
```
### 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:
If you wish to override the default port (8000), use `--port` or `-p`:
```sh
$ ws --port 9000
serving at http://localhost:9000
$ ws --rewrite '/css/style.css -> /build/css/style.css'
```
To add compression, reducing bandwidth, increasing page load time (by 10-15% on my Macbook Air)
Or, more generally (matching any stylesheet under `/css`):
```sh
$ ws --compress
$ ws --rewrite '/css/:stylesheet -> /build/css/:stylesheet'
```
### Logging
Passing a value to `--log-format` will write an access log to `stdout`.
With a deep CSS directory structure it may be easier to mount the entire contents of `/build/css` to the `/css` path:
Either use a built-in [morgan](https://github.com/expressjs/morgan) logger preset:
```sh
$ ws --log-format short
$ ws --rewrite '/css/* -> /build/css/$1'
```
Or a custom [morgan](https://github.com/expressjs/morgan) log format:
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 -f ':method -> :url'
$ ws --rewrite '/npm/* -> http://registry.npmjs.org/$1'
```
Or silence:
Map local requests for repo data to the Github API:
```sh
$ ws -f none
$ ws --rewrite '/:user/repos/:name -> https://api.github.com/repos/:user/:name'
```
## 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`:
### Stored config
Use the same port and blacklist every time? Persist it to `package.json`:
```json
{
"name": "my-project",
"version": "0.11.8",
"local-web-server":{
"port": 8100
"name": "example",
"version": "1.0.0",
"local-web-server": {
"port": 8100,
"forbid": "*.json"
}
}
```
Or in a `.local-web-server.json` file stored in the directory you want to serve (typically the root folder of your site):
or `.local-web-server.json`
```json
{
"port": 8100,
"log-format": "tiny"
"forbid": "*.json"
}
```
Or store global defaults in a `.local-web-server.json` file in your home directory.
```json
{
"port": 3000,
"refresh-rate": 1000
}
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. Command-line options take precedence over all.
To inspect stored config, run:
```sh
$ ws --config
```
All stored defaults are overriden by options supplied at the command line.
### Logging
By default, local-web-server outputs a simple, dynamic statistics view. To see traditional web server logs, use `--log-format`:
To view your stored defaults, run:
```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.
### Other usage
#### Compression
Serve gzip-compressed resources, where applicable
```sh
$ ws --config
$ ws --compress
```
## 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:
#### 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" ]
}
"mime": {
"text/plain": [ "php", "pl" ]
}
}
```
## Use with Logstalgia
local-web-server is compatible with [logstalgia](http://code.google.com/p/logstalgia/).
#### Log Visualisation
Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/75lb/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`.
### Install Logstalgia
On MacOSX, install with [homebrew](http://brew.sh):
```sh
$ brew install logstalgia
$ npm install -g local-web-server
```
Alternatively, [download a release for your system from github](https://github.com/acaudwell/Logstalgia/releases/latest).
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
Then pipe the `logstalgia` output format directly into logstalgia for real-time visualisation:
```sh
$ ws -f logstalgia | logstalgia -
$ 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"
}
}
```
![local-web-server with logstalgia](http://75lb.github.io/local-web-server/logstagia.gif)
3\. Document how to build and launch your site
## 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
$ npm install
$ npm start
serving at http://localhost:8100
```
Then specify this file in your glTail config:
## API Reference
<a name="module_local-web-server"></a>
## local-web-server
<a name="exp_module_local-web-server--localWebServer"></a>
### localWebServer([options]) ⏏
Returns a Koa application
```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
**Kind**: Exported function
**Params**
- [options] <code>object</code> - options
- [.static] <code>object</code> - koajs/static config
- [.root] <code>string</code> - root directory
- [.options] <code>string</code> - options
- [.serveIndex] <code>object</code> - koa-serve-index config
- [.path] <code>string</code> - root directory
- [.options] <code>string</code> - options
- [.forbid] <code>Array.&lt;string&gt;</code> - a list of forbidden routes.
**Example**
```js
const localWebServer = require('local-web-server')
localWebServer().listen(8000)
```
&copy; 2015 Lloyd Brookes <75pound@gmail.com>
* * *
&copy; 2015 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown).

99
bin/cli.js

@ -0,0 +1,99 @@
#!/usr/bin/env node
'use strict'
const localWebServer = require('../')
const cliOptions = require('../lib/cli-options')
const commandLineArgs = require('command-line-args')
const ansi = require('ansi-escape-sequences')
const loadConfig = require('config-master')
const path = require('path')
const cli = commandLineArgs(cliOptions.definitions)
const usage = cli.getUsage(cliOptions.usageData)
const stored = loadConfig('local-web-server')
const options = collectOptions()
// TODO summary line on server launch
if (options.misc.help) {
console.log(usage)
process.exit(0)
}
if (options.misc.config) {
console.log(JSON.stringify(options.server, null, ' '))
process.exit(0)
}
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
}).listen(options.server.port, onServerUp)
function halt (err) {
console.log(ansi.format(`Error: ${err.message}`, 'red'))
console.log(usage)
process.exit(1)
}
function onServerUp () {
console.error(ansi.format(
path.resolve(options.server.directory) === process.cwd()
? `serving at [underline]{http://localhost:${options.server.port}}`
: `serving [underline]{${options.server.directory}} at [underline]{http://localhost:${options.server.port}}`
))
}
function collectOptions () {
let options = {}
/* parse command line args */
try {
options = cli.parse()
} catch (err) {
halt(err)
}
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]
}
})
}

155
bin/ws.js

@ -1,155 +0,0 @@
#!/usr/bin/env node
'use strict'
var dope = require('console-dope')
var http = require('http')
var cliArgs = require('command-line-args')
var o = require('object-tools')
var t = require('typical')
var path = require('path')
var loadConfig = require('config-master')
var homePath = require('home-path')
var logStats = require('stream-log-stats')
var connect = require('connect')
var morgan = require('morgan')
var serveStatic = require('serve-static')
var directory = require('serve-index')
var compress = require('compression')
var cliOptions = require('../lib/cli-options')
/* specify the command line arg definitions and usage forms */
var cli = cliArgs(cliOptions)
var usage = cli.getUsage({
title: 'local-web-server',
description: 'Lightweight static web server, zero configuration.',
footer: 'Project home: [underline]{https://github.com/75lb/local-web-server}',
usage: {
forms: [
'$ ws <server options>',
'$ ws --config',
'$ ws --help'
]
},
groups: {
server: 'Server',
misc: 'Misc'
}
})
/* parse command line args */
try {
var wsOptions = cli.parse()
} catch (err) {
halt(err.message)
}
/* Load and merge together options from
- ~/.local-web-server.json
- {cwd}/.local-web-server.json
- the `local-web-server` property of {cwd}/package.json
*/
var storedConfig = loadConfig(
path.join(homePath(), '.local-web-server.json'),
path.join(process.cwd(), '.local-web-server.json'),
{ jsonPath: path.join(process.cwd(), 'package.json'), configProperty: 'local-web-server' }
)
var builtInDefaults = {
port: 8000,
directory: process.cwd(),
'refresh-rate': 500,
mime: {}
}
/* override built-in defaults with stored config and then command line args */
wsOptions.server = o.extend(builtInDefaults, storedConfig, wsOptions.server)
/* user input validation */
if (!t.isNumber(wsOptions.server.port)) {
halt('please supply a numeric port value')
}
if (wsOptions.misc.config) {
dope.log('Stored config: ')
dope.log(storedConfig)
process.exit(0)
} else if (wsOptions.misc.help) {
dope.log(usage)
} else {
process.on('SIGINT', function () {
dope.showCursor()
dope.log()
process.exit(0)
})
dope.hideCursor()
launchServer()
/* write launch information to stderr (stdout is reserved for web log output) */
if (path.resolve(wsOptions.server.directory) === process.cwd()) {
dope.error('serving at %underline{%s}', 'http://localhost:' + wsOptions.server.port)
} else {
dope.error('serving %underline{%s} at %underline{%s}', wsOptions.server.directory, 'http://localhost:' + wsOptions.server.port)
}
}
function halt (message) {
dope.red.log('Error: %s', message)
dope.log(usage)
process.exit(1)
}
function launchServer () {
var app = connect()
/* enable cross-origin requests on all resources */
app.use(function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*')
next()
})
if (wsOptions.server['log-format'] !== 'none') app.use(getLogger())
/* --compress enables compression */
if (wsOptions.server.compress) app.use(compress())
/* set the mime-type overrides specified in the config */
serveStatic.mime.define(wsOptions.server.mime)
/* enable static file server, including directory browsing support */
app.use(serveStatic(path.resolve(wsOptions.server.directory)))
.use(directory(path.resolve(wsOptions.server.directory), { icons: true }))
/* launch server */
http.createServer(app)
.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
halt('port ' + wsOptions.server.port + ' is already is use')
} else {
halt(err.message)
}
})
.listen(wsOptions.server.port)
}
function getLogger () {
/* log using --log-format (if supplied) */
var logFormat = wsOptions.server['log-format']
if (logFormat) {
if (logFormat === 'logstalgia') {
/* customised logger :date token, purely to satisfy Logstalgia. */
morgan.token('date', function () {
var d = new Date()
return (d.getDate() + '/' + d.getUTCMonth() + '/' + d.getFullYear() + ':' + d.toTimeString())
.replace('GMT', '').replace(' (BST)', '')
})
logFormat = 'combined'
}
return morgan(logFormat)
/* if no `--log-format` was specified, pipe the default format output
into `log-stats`, which prints statistics to the console */
} else {
return morgan('common', { stream: logStats({ refreshRate: wsOptions.server['refresh-rate'] }) })
}
}

BIN
doc/img/logstagia.gif

After

Width: 700  |  Height: 360  |  Size: 570 KiB

37
doc/visualisation.md

@ -0,0 +1,37 @@
## Goaccess
## Logstalgia
local-web-server is compatible with [logstalgia](http://code.google.com/p/logstalgia/).
### Install Logstalgia
On MacOSX, install with [homebrew](http://brew.sh):
```sh
$ brew install logstalgia
```
Alternatively, [download a release for your system from github](https://github.com/acaudwell/Logstalgia/releases/latest).
Then pipe the `logstalgia` output format directly into logstalgia for real-time visualisation:
```sh
$ ws -f logstalgia | logstalgia -
```
![local-web-server with logstalgia](https://raw.githubusercontent.com/75lb/local-web-server/master/doc/img/logstagia.gif)
## glTail
To use with [glTail](http://www.fudgie.org), write your log to disk using the "default" format:
```sh
$ ws -f default > web.log
```
Then specify this file in your glTail config:
```yaml
servers:
dev:
host: localhost
source: local
files: /Users/Lloyd/Documents/MySite/web.log
parser: apache
color: 0.2, 0.2, 1.0, 1.0
```

5
example/forbid/.local-web-server.json

@ -0,0 +1,5 @@
{
"forbid": [
"/admin/*", "*.php"
]
}

1
example/forbid/admin/blocked.html

@ -0,0 +1 @@
<h1>Forbidden page</h1>

1
example/forbid/allowed.html

@ -0,0 +1 @@
<h1>A permitted page</h1>

5
example/forbid/index.html

@ -0,0 +1,5 @@
<h1>Forbidden routes</h1>
<p>
Notice you can access <a href="allowed.html">this page</a>, but not <a href="admin/blocked.html">this admin page</a> or <a href="something.php">php file</a>.
</p>

0
test/something.php → example/forbid/something.php

5
example/mime-override/.local-web-server.json

@ -0,0 +1,5 @@
{
"mime": {
"text/plain": [ "php" ]
}
}

1
example/mime-override/something.php

@ -0,0 +1 @@
<?php echo "i'm coding PHP templatez!\n" ?>

7
example/rewrite/.local-web-server.json

@ -0,0 +1,7 @@
{
"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" }
]
}

4
example/rewrite/build/styles/style.css

@ -0,0 +1,4 @@
body {
font-family: monospace;
font-size: 1.3em;
}

22
example/rewrite/index.html

@ -0,0 +1,22 @@
<head>
<link rel="stylesheet" href="css/style.css">
</head>
<h1>Rewriting paths</h1>
<h2>Config</h2>
<pre><code>
{
"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" }
]
}
</code></pre>
<h2>Links</h2>
<ul>
<li><a href="/css/style.css">/css/style.css</li>
<li><a href="/npm/local-web-server">/npm/local-web-server</a></li>
<li><a href="/75lb/repos/work">/75lb/repos/work</a></li>
</ul>

7
example/simple/css/style.css

@ -0,0 +1,7 @@
body {
background-color: #AA3939;
color: #FFE2E2
}
svg {
fill: #000
}

10
example/simple/index.html

@ -0,0 +1,10 @@
<head>
<link rel="stylesheet" href="css/style.css">
</head>
<h1>Amazing Page</h1>
<p>
With a freaky triangle..
</p>
<svg width="500" height="500">
<polygon points="250,0 0,500 500,500"></polygon>
</svg>

7
example/simple/package.json

@ -0,0 +1,7 @@
{
"name": "example",
"version": "1.0.0",
"local-web-server": {
"port": 8100
}
}

3
example/spa/.local-web-server.json

@ -0,0 +1,3 @@
{
"spa": "index.html"
}

3
example/spa/css/style.css

@ -0,0 +1,3 @@
body {
background-color: IndianRed;
}

8
example/spa/index.html

@ -0,0 +1,8 @@
<head>
<link rel="stylesheet" href="/css/style.css">
</head>
<h1>Single Page App</h1>
<h2>Location: <span></span></h2>
<script>
document.querySelector('h2 span').textContent = window.location.pathname
</script>

210
jsdoc2md/README.hbs

@ -0,0 +1,210 @@
[![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/75lb/local-web-server.svg?branch=master)](https://travis-ci.org/75lb/local-web-server)
[![Dependency Status](https://david-dm.org/75lb/local-web-server.svg)](https://david-dm.org/75lb/local-web-server)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard)
# local-web-server
A simple web-server for productive front-end development.
**Requires node v4.0.0 or higher**.
## Synopsis
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).
### Static site
Fire up your static site on the default port:
```sh
$ ws
serving at http://localhost:8000
```
### 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 urls (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.*
### 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
```
### 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'
```
### Stored config
Use the same port and blacklist every time? Persist it 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. Command-line options 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.
### Other usage
#### 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" ]
}
}
```
#### Log Visualisation
Instructions for how to visualise log output using goaccess, logstalgia or gltail [here](https://github.com/75lb/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
{{>main}}
* * *
&copy; 2015 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown).

82
lib/cli-options.js

@ -1,30 +1,54 @@
module.exports = [
{
name: 'port', alias: 'p', type: Number, defaultOption: true,
description: 'Web server port', 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 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: 'directory', alias: 'd', type: String,
description: 'Root directory, defaults to the current directory', group: 'server'
},
{
name: 'compress', alias: 'c', type: Boolean,
description: 'Enable gzip compression, reduces bandwidth.', group: 'server'
},
{
name: 'refresh-rate', alias: 'r', type: Number,
description: 'Statistics view refresh rate in ms. Defaults to 500.', 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'
module.exports = {
definitions: [
{
name: 'port', alias: 'p', type: Number, defaultOption: true,
description: 'Web server port', 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: 'directory', alias: 'd', type: String,
description: 'Root directory, defaults to the current directory', group: 'server'
},
{
name: 'compress', alias: 'c', type: Boolean,
description: 'Enable gzip compression, reduces bandwidth.', group: 'server'
},
{
name: 'forbid', alias: 'b', type: String, multiple: true, typeLabel: '[underline]{regexp} ...',
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: 'rewrite', alias: 'r', type: String, multiple: true, typeLabel: '[underline]{expression} ...',
description: 'A list of URL rewrite rules', group: 'server'
},
{
name: 'help', alias: 'h', type: Boolean,
description: 'Print these usage instructions', group: 'misc'
},
{
name: 'config', type: Boolean,
description: 'Print the config found in [underline]{package.json} and/or [underline]{.local-web-server}', group: 'misc'
}
],
usageData: {
title: 'local-web-server',
description: 'A simple web-server for productive front-end development.',
footer: 'Project home: [underline]{https://github.com/75lb/local-web-server}',
synopsis: [
'$ ws [server options]',
'$ ws --config',
'$ ws --help'
],
groups: {
server: 'Server',
misc: 'Misc'
}
}
]
}

193
lib/local-web-server.js

@ -0,0 +1,193 @@
'use strict'
const path = require('path')
const http = require('http')
const url = require('url')
const Koa = require('koa')
const convert = require('koa-convert')
const cors = require('kcors')
const _ = require('koa-route')
const pathToRegexp = require('path-to-regexp')
/**
* @module local-web-server
*/
module.exports = localWebServer
/**
* Returns a Koa application
*
* @param [options] {object} - options
* @param [options.static] {object} - koajs/static config
* @param [options.static.root] {string} - root directory
* @param [options.static.options] {string} - options
* @param [options.serveIndex] {object} - koa-serve-index config
* @param [options.serveIndex.path] {string} - root directory
* @param [options.serveIndex.options] {string} - options
* @param [options.forbid] {string[]} - a list of forbidden routes.
*
* @alias module:local-web-server
* @example
* const localWebServer = require('local-web-server')
* localWebServer().listen(8000)
*/
function localWebServer (options) {
options = Object.assign({
static: {},
serveIndex: {},
log: {},
compress: false,
mime: {},
forbid: [],
rewrite: []
}, options)
const log = options.log
log.options = log.options || {}
const app = new Koa()
const _use = app.use
app.use = x => _use.call(app, convert(x))
/* CORS: allow from any origin */
app.use(cors())
/* rewrite rules */
if (options.rewrite && options.rewrite.length) {
options.rewrite.forEach(route => {
if (route.to) {
if (url.parse(route.to).host) {
app.use(_.all(route.from, proxyRequest(route)))
} else {
const rewrite = require('koa-rewrite')
app.use(rewrite(route.from, route.to))
}
}
})
}
/* path blacklist */
if (options.forbid.length) {
app.use(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) {
app.use((ctx, next) => {
return next().then(() => {
const reqPathExtension = path.extname(ctx.path).slice(1)
Object.keys(options.mime).forEach(mimeType => {
const extsToOverride = options.mime[mimeType]
if (extsToOverride.indexOf(reqPathExtension) > -1) ctx.type = mimeType
})
})
})
}
/* compress response */
if (options.compress) {
const compress = require('koa-compress')
app.use(compress())
}
/* Logging */
if (log.format !== 'none') {
const morgan = require('koa-morgan')
if (!log.format) {
const streamLogStats = require('stream-log-stats')
log.options.stream = streamLogStats({ refreshRate: 500 })
app.use(morgan.middleware('common', log.options))
} else if (log.format === 'logstalgia') {
morgan.token('date', logstalgiaDate)
app.use(morgan.middleware('combined', log.options))
} else {
app.use(morgan.middleware(log.format, log.options))
}
}
/* 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))
}
/* for any URL not matched by static (e.g. `/search`), serve the SPA */
if (options.spa) {
const send = require('koa-send')
app.use(_.all('*', function * () {
yield send(this, options.spa, { root: process.cwd() })
}))
}
return app
}
function logstalgiaDate () {
var d = new Date()
return (`${d.getDate()}/${d.getUTCMonth()}/${d.getFullYear()}:${d.toTimeString()}`).replace('GMT', '').replace(' (BST)', '')
}
function proxyRequest (route) {
const httpProxy = require('http-proxy')
const proxy = httpProxy.createProxyServer({
changeOrigin: true
})
return function proxyMiddleware (ctx) {
const next = arguments[arguments.length - 1]
const keys = []
route.re = pathToRegexp(route.from, keys)
route.new = ctx.path.replace(route.re, route.to)
keys.forEach((key, index) => {
const re = RegExp(`:${key.name}`, 'g')
route.new = route.new
.replace(re, arguments[index + 1] || '')
})
/* test no keys remain in the new path */
keys.length = 0
pathToRegexp(route.new, keys)
if (keys.length) {
ctx.throw(500, `[PROXY] Invalid target URL: ${route.new}`)
return next()
}
ctx.response = false
proxy.once('error', err => {
ctx.throw(500, `[PROXY] ${err.message}: ${route.new}`)
})
proxy.once('proxyReq', function (proxyReq) {
proxyReq.path = url.parse(route.new).path;
})
proxy.web(ctx.req, ctx.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()
}
}
}
process.on('unhandledRejection', (reason, p) => {
throw reason
})

63
package.json

@ -1,39 +1,54 @@
{
"name": "local-web-server",
"version": "0.5.23",
"description": "Lightweight static web server, zero configuration. Perfect for front-end devs.",
"description": "A simple web-server for productive front-end development",
"bin": {
"ws": "./bin/ws.js"
"ws": "./bin/cli.js"
},
"main": "lib/local-web-server.js",
"license": "MIT",
"keywords": [
"dev",
"server",
"web",
"tool",
"front-end",
"development",
"cors",
"mime",
"rest"
],
"engines": {
"node": ">=0.10.0"
"node": ">=4.0.0"
},
"scripts": {
"lint": "jshint bin/ws.js; echo;"
"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"
},
"repository": "https://github.com/75lb/local-web-server",
"author": "Lloyd Brookes",
"author": "Lloyd Brookes <75pound@gmail.com>",
"dependencies": {
"command-line-args": "^1.0.0",
"compression": "^1.0.2",
"config-master": "^1",
"connect": "^3.0.0",
"console-dope": "~0.3.0",
"home-path": "^1",
"morgan": "^1.0.0",
"object-tools": "^2",
"proxy-middleware": "^0.13.0",
"serve-index": "^1.6.3",
"serve-static": "^1.8",
"stream-log-stats": "^1",
"typical": "^2.0.0"
"command-line-args": "^2.0.2",
"config-master": "^2",
"http-proxy": "^1.12.0",
"kcors": "^1.0.1",
"koa": "^2.0.0-alpha.3",
"koa-compress": "^1.0.8",
"koa-conditional-get": "^1.0.3",
"koa-convert": "^1.1.0",
"koa-etag": "^2.1.0",
"koa-morgan": "^0.4.0",
"koa-rewrite": "^1.1.1",
"koa-route": "^3",
"koa-send": "^3.1.0",
"koa-serve-index": "^1.1.0",
"koa-static": "^1.5.2",
"path-to-regexp": "^1.2.1",
"stream-log-stats": "^v1.1.0-0"
},
"local-web-server": {
"mime": {
"text/plain": [
"php"
]
}
"devDependencies": {
"req-then": "^0.2.2",
"tape": "^4.2.2"
}
}

5
test/.local-web-server.json

@ -1,5 +0,0 @@
{
"mime": {
"text/plain": [ "php" ]
}
}

4
test/ajax.html → test/fixture/ajax.html

@ -9,10 +9,10 @@
<script>
var $ = document.querySelector.bind(document);
var req = new XMLHttpRequest();
req.open("get", "http://localhost:8000/README.md", true);
req.open("get", "http://localhost:8000/big-file.txt", true);
req.onload = function(){
$("#readme").textContent = this.responseText;
}
req.send()
</script>
</body>
</body>

195
test/fixture/big-file.txt

@ -0,0 +1,195 @@
[![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/75lb/local-web-server.svg)](https://david-dm.org/75lb/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 <server options>
$ ws --config
$ ws --help
Server
-p, --port <number> Web server port
-f, --log-format <string> 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 <string> Root directory, defaults to the current directory
-c, --compress Enable gzip compression, reduces bandwidth.
-r, --refresh-rate <number> 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
```
* * *
&copy; 2015 Lloyd Brookes <75pound@gmail.com>

1
test/fixture/file.txt

@ -0,0 +1 @@
test

1
test/fixture/forbid/one.html

@ -0,0 +1 @@
one

1
test/fixture/forbid/two.php

@ -0,0 +1 @@
<?php echo "i'm coding PHP templatez!\n" ?>

1
test/fixture/one/file.txt

@ -0,0 +1 @@
one

1
test/fixture/rewrite/one.html

@ -0,0 +1 @@
one

1
test/fixture/something.php

@ -0,0 +1 @@
<?php echo "i'm coding PHP templatez!\n" ?>

147
test/test.js

@ -0,0 +1,147 @@
'use strict'
const test = require('tape')
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(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.strictEqual(response.data, 'test\n')
}})
})
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('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.strictEqual(response.data, 'one\n')
}})
})
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))
}})
})
Loading…
Cancel
Save