Lloyd Brookes
9 years ago
39 changed files with 1223 additions and 342 deletions
-
1.coveralls.yml
-
1.gitignore
-
18.jshintrc
-
4.travis.yml
-
256README.md
-
99bin/cli.js
-
155bin/ws.js
-
BINdoc/img/logstagia.gif
-
37doc/visualisation.md
-
5example/forbid/.local-web-server.json
-
1example/forbid/admin/blocked.html
-
1example/forbid/allowed.html
-
5example/forbid/index.html
-
0example/forbid/something.php
-
5example/mime-override/.local-web-server.json
-
1example/mime-override/something.php
-
7example/rewrite/.local-web-server.json
-
4example/rewrite/build/styles/style.css
-
22example/rewrite/index.html
-
7example/simple/css/style.css
-
10example/simple/index.html
-
7example/simple/package.json
-
3example/spa/.local-web-server.json
-
3example/spa/css/style.css
-
8example/spa/index.html
-
210jsdoc2md/README.hbs
-
82lib/cli-options.js
-
193lib/local-web-server.js
-
63package.json
-
5test/.local-web-server.json
-
2test/fixture/ajax.html
-
195test/fixture/big-file.txt
-
1test/fixture/file.txt
-
1test/fixture/forbid/one.html
-
1test/fixture/forbid/two.php
-
1test/fixture/one/file.txt
-
1test/fixture/rewrite/one.html
-
1test/fixture/something.php
-
147test/test.js
@ -0,0 +1 @@ |
|||||
|
repo_token: w9HmlMl9558e1LpP9p62YgYutkVE9PqtN |
@ -1 +1,2 @@ |
|||||
node_modules |
node_modules |
||||
|
tmp |
@ -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 |
|
||||
} |
|
@ -0,0 +1,4 @@ |
|||||
|
language: node_js |
||||
|
node_js: |
||||
|
- '5.0' |
||||
|
- '4.2' |
@ -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) |
[![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) |
[![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) |
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://github.com/feross/standard) |
||||
|
|
||||
# local-web-server |
# 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 |
```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 |
```sh |
||||
$ npm install |
|
||||
$ npm install -g local-web-server |
|
||||
$ ws |
$ 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 |
```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 |
```sh |
||||
$ ws |
|
||||
|
$ ws --forbid '*.json' '*.yml' |
||||
serving at http://localhost:8000 |
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 |
```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 |
```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 |
```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 |
```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 |
```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 |
```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 |
```json |
||||
{ |
{ |
||||
"port": 8100, |
"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 |
```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 |
```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 |
```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 |
```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 |
```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.<string></code> - a list of forbidden routes. |
||||
|
|
||||
|
**Example** |
||||
|
```js |
||||
|
const localWebServer = require('local-web-server') |
||||
|
localWebServer().listen(8000) |
||||
``` |
``` |
||||
|
|
||||
© 2015 Lloyd Brookes <75pound@gmail.com> |
|
||||
|
* * * |
||||
|
|
||||
|
© 2015 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). |
@ -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] |
||||
|
} |
||||
|
}) |
||||
|
} |
@ -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'] }) }) |
|
||||
} |
|
||||
} |
|
After Width: 700 | Height: 360 | Size: 570 KiB |
@ -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 |
||||
|
``` |
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"forbid": [ |
||||
|
"/admin/*", "*.php" |
||||
|
] |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<h1>Forbidden page</h1> |
@ -0,0 +1 @@ |
|||||
|
<h1>A permitted page</h1> |
@ -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,0 +1,5 @@ |
|||||
|
{ |
||||
|
"mime": { |
||||
|
"text/plain": [ "php" ] |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<?php echo "i'm coding PHP templatez!\n" ?>
|
@ -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" } |
||||
|
] |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
body { |
||||
|
font-family: monospace; |
||||
|
font-size: 1.3em; |
||||
|
} |
@ -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> |
@ -0,0 +1,7 @@ |
|||||
|
body { |
||||
|
background-color: #AA3939; |
||||
|
color: #FFE2E2 |
||||
|
} |
||||
|
svg { |
||||
|
fill: #000 |
||||
|
} |
@ -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> |
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"name": "example", |
||||
|
"version": "1.0.0", |
||||
|
"local-web-server": { |
||||
|
"port": 8100 |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"spa": "index.html" |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
body { |
||||
|
background-color: IndianRed; |
||||
|
} |
@ -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> |
@ -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}} |
||||
|
|
||||
|
* * * |
||||
|
|
||||
|
© 2015 Lloyd Brookes <75pound@gmail.com>. Documented by [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown). |
@ -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' |
||||
|
} |
||||
} |
} |
||||
] |
|
||||
|
} |
@ -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 |
||||
|
}) |
@ -1,39 +1,54 @@ |
|||||
{ |
{ |
||||
"name": "local-web-server", |
"name": "local-web-server", |
||||
"version": "0.5.23", |
"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": { |
"bin": { |
||||
"ws": "./bin/ws.js" |
|
||||
|
"ws": "./bin/cli.js" |
||||
}, |
}, |
||||
|
"main": "lib/local-web-server.js", |
||||
"license": "MIT", |
"license": "MIT", |
||||
|
"keywords": [ |
||||
|
"dev", |
||||
|
"server", |
||||
|
"web", |
||||
|
"tool", |
||||
|
"front-end", |
||||
|
"development", |
||||
|
"cors", |
||||
|
"mime", |
||||
|
"rest" |
||||
|
], |
||||
"engines": { |
"engines": { |
||||
"node": ">=0.10.0" |
|
||||
|
"node": ">=4.0.0" |
||||
}, |
}, |
||||
"scripts": { |
"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", |
"repository": "https://github.com/75lb/local-web-server", |
||||
"author": "Lloyd Brookes", |
|
||||
|
"author": "Lloyd Brookes <75pound@gmail.com>", |
||||
"dependencies": { |
"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" |
||||
} |
} |
||||
} |
} |
@ -1,5 +0,0 @@ |
|||||
{ |
|
||||
"mime": { |
|
||||
"text/plain": [ "php" ] |
|
||||
} |
|
||||
} |
|
@ -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 |
||||
|
``` |
||||
|
|
||||
|
* * * |
||||
|
|
||||
|
© 2015 Lloyd Brookes <75pound@gmail.com> |
@ -0,0 +1 @@ |
|||||
|
test |
@ -0,0 +1 @@ |
|||||
|
one |
@ -0,0 +1 @@ |
|||||
|
<?php echo "i'm coding PHP templatez!\n" ?>
|
@ -0,0 +1 @@ |
|||||
|
one |
@ -0,0 +1 @@ |
|||||
|
one |
@ -0,0 +1 @@ |
|||||
|
<?php echo "i'm coding PHP templatez!\n" ?>
|
@ -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)) |
||||
|
}}) |
||||
|
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue