Lloyd Brookes
9 years ago
39 changed files with 1223 additions and 342 deletions
-
1.coveralls.yml
-
1.gitignore
-
18.jshintrc
-
4.travis.yml
-
248README.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
-
36lib/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 |
|||
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' |
@ -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). |
@ -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", |
|||
"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" |
|||
} |
|||
} |
@ -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