Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mvp #1

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules

# Generated output
lib
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
!lib/*
!lib/**/*
16 changes: 16 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
language: node_js
node_js:
- node
- iojs
- '4'
- '0.12'
install: make clean build
script: echo 'no tests.'
deploy:
provider: npm
email: [email protected]
api_key:
secure: B9d/SScqDGVygDIM1LCq+HKJzOEq4aeQMM22Gk7HAtz9bGqWHsn+pkkZNOXS4K6KaylDHR1/94G7sdIJHWIteOUE9u3Bp7ORwgjpDk5uRrvK7A/74tmhNgOAS1JenUOEhoZ05GxGkXMjPJE6rThPBiS1vAUtBmDV1zi9KEC/2hf46icpix4rvybEplukaW+WKZrgzau1ROO0IvCOwh/pKaB+PIrnzRYsGSfFfYGpZNIA1PBwqAaeAuGQ5QKd4Q1uXsEfmpqSRFS8m+PnXJ2kQv5aUDfJiGzpq3K8mraa0n1/Faj2GOiGdnF/bvyjDQZMSIu4Zy8+/sflQwxMHZYCsZPS9rqwiiAMnpDmhjiSfVPYEzXg0ckPETY+o/WJ8OK0xvd7c411h5nb/dIuS3pbRZd1v9MJpeGszS7Qm+TMTJ6sKfShd8gyvD1TA2pmc2tB/S/GA1QWGuf7993k/ZCiagoXtYmP7iwWEK05CJLkmtcf5IDKgIDuZ3ABtGmWejd/DGQi6x3BSrYdDeRtA7JyE2V5ACbX3BqIO2zF8JBsprPWvLbuKnrjV++sM4g65qIziP1fVXlI4G7HprUIABA+v2IVQLtp+M3A7Hb08SwtiaLfcCLNOtfOcG3yEAkciMHRBV4DA4e2cMt3NlOoCcZI2JTyvw7/DzLkqt8irFyvJc8=
on:
tags: true
repo: websdk/servera
29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
SHELL := /bin/bash
PATH := $(shell echo $${PATH//\.\/node_modules\/\.bin:/}):node_modules/.bin

SRC = $(wildcard src/*.js) $(wildcard src/**/*.js)
LIB = $(SRC:src/%=lib/%)
TST = $(wildcard test/*.js) $(wildcard test/**/*.js)
NPM = npm install --local > /dev/null && touch node_modules

CJS = babel --source-maps --presets es2015
WATCH = nodemon -w src -x

.PHONY: build dev clean

build: node_modules $(LIB)

dev: node_modules
$(WATCH) "clear && time $(MAKE) --debug=b build | tail -n +8"

clean:
@rm -rf $$(cat .gitignore)

lib/%.js: src/%.js
@mkdir -p $(@D)
@$(CJS) $< > $@

node_modules: package.json
$(NPM)
node_modules/%:
$(NPM)
112 changes: 111 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,112 @@
# servera
Simple web development server

Simple web development server focused on two things:

- Serve things
- Provide hooks for change notifications

**NOTE:** This server must not be used in production. It isn't nearly as fast as many other available products, and probably has many severe security issues. Do not – under any circumstance – use this for anything but local web development purposes.

# Installation

```bash
$ npm install --save-dev @websdk/servera
```

# Usage

```bash
usage: servera [<root>] [options ...]

arguments:

<root>

The root path from which files will be served. Clients will not be able to request files outside of this path. Symbolic links will not be resolved. (Default: current working directory.)

options:

-h,--help

Prints this information.

-p,--port=<pattern>

Port to listen on. The following patterns are valid:

- Single numeric value: e.g. `8080`
- Inclusive range: `8080-9090`
- Random: `random` or `0`
- Combinations: `1337,8080-9090,random`

If multiple patterns are used, they are attempted in order until one works; or the list of ports is exhausted in which case the server exits with an error. (Default: `random`.)

-a,--address=<value>

Address to listen on. If omitted, the server will accept connections on any IPv4 address (0.0.0.0).

--no-dir

By default, the server will generate a directory listing whenever the requested URL path refers to a path under `<root>`. If this option is present, this behaviour is disabled.

-s,--silent

Suppresses all messages except errors.

-o,--open[=<browser>]

Opens a browser after starting the server. Also enables a shortcut to open a browser, whenever pressed in the terminal. The shortcut is presented in the terminal output when the server starts. The list of available browsers is dependant on environment, and unless `<browser>` is specified whatever is the system default will be opened. Use `--no-open` to avoid opening a browser.

-c,--cache[=<seconds>]

Controls how resources are cached. E.g. to cache resources for 30 seconds, use `-c 10` or `--cache 10`. Set to 0 to disable caching. (Default: 0)

--proxy=<url>

If set, the server will proxy any requests that can't be resolved to `<url>`. The request is not redirected, so the URL doesn't change, but whatever response is returned from `<url>` is proxied. This can be used to proxy paths.

--cors

Enable CORS.
```

## Triggering updates

When the server has started, it is possible to trigger updates by sending a request to `./well-known/updates/{path}` – where `{path}` is the path relative to `<root>` that was updated. The following content types of this update request are supported:

- `application/json`: the body *must* be an array of strings, which represent the paths relative to `<root>` that should be updated
- `text/plain`: the body will be split on line breaks, and each line should be a path relative to `<root>`

The HTTP method of the request represents certain semantics, and trigger different events:

- `PUT` will trigger a `create` event
- `POST` will trigger an `update` event
- `DELETE` will trigger a `delete` event

Any other method, including `GET`, is not supported.

## Handling updates

Clients that opt to polling only have to `GET ./well-known/updates` to retrieve the updates of all files under `<root>`. This will include *all* updates, based on file creation and modification times. Because of this, files that were deleted will not be present. The server will be able to return updates in `application/json` or `text/plain` format.

Clients may opt to stream updates, in which case they will not only receive `create` and `update` events, but also `delete` events. To do this, clients should request `Content-Type: text/event-stream`. This will return a stream of events, whenever files are created, updated, or deleted.

To listen for updates, a client can `GET ./well-known/updates`. If the content type of the request is `Content-Type: text/event-stream` a stream of events will be returned.

# Rationale

Most development servers try too hard to please, and do things like watch for file changes and what not. These might seem like *good things* but they really aren't. It's incredibly difficult, perhaps impossible even, to generalize logic for dealing with project updates and particularly so in the rapidly changing world of web development. Some questions that inevitably arise when trying to create a highly interactive development environment:

- Which files should trigger reloads?
- Is a reload a full page reload?
- Can bits of code be replaced without replacing others?
- Should styles be reloaded?
- How do you notify connected clients?
- Must developers include special logic to deal with all this?
- Why is all the rum gone?

The approach of this server is the eschew all those responsibilities that inexorably ends up in its lap, were it to actually watch for changes. Instead, it provides APIs to easily notify the server that some files changed, and that those files should trigger some sort of update. When such a trigger happens, it lets the client know – if it cares – such that the *client* can deal with those events.

--

[License](LICENSE)
36 changes: 36 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@websdk/servera",
"version": "0.0.1-mvp.0",
"description": "Simple web development server focused on two things:",
"bin": {
"servera": "lib/cli.js"
},
"main": "lib/api.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/websdk/servera.git"
},
"author": "Marcus Stade <[email protected]> (http://madebystade.se/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/websdk/servera/issues"
},
"homepage": "https://github.com/websdk/servera#readme",
"dependencies": {
"cliui": "3.1.0",
"ecstatic": "1.3.1",
"funkis": "0.2.0",
"opener": "1.4.1",
"request": "2.67.0",
"window-size": "0.1.4",
"yargs": "3.31.0"
},
"devDependencies": {
"babel-cli": "6.3.17",
"babel-preset-es2015": "6.3.13",
"source-map-support": "0.4.0"
}
}
87 changes: 87 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
export default createServer

import getPorts from './getPorts'
import { is, each, assert, partial as $ } from 'funkis'
import { accessSync as exists } from 'fs'
import createStaticServer from 'ecstatic'
import http from 'http'
import opener from 'opener'
import dns from 'dns'
import request from 'request'

import { hostname, networkInterfaces as ifconfig } from 'os'

function createServer(
{ root = process.cwd()
, port = 'random'
, address = '0.0.0.0'
, showDir = true
, silent = false
, open = 'default'
, cache = 0
, cors = false
, proxy
}
) {
assert($(exists, root), 'root path does not exist')

let ports = getPorts(port)
let serveFiles = createStaticServer(
{ root
, cors
, cache
, showDir
, gzip: false
, handleError: !proxy
}
)

let server = http.createServer((req, res) => {
serveFiles(req, res, () => {
if (proxy.match(/^http:/)) {
req.pipe(request(proxy)).pipe(res)
} else {
req.url = proxy
serveFiles(req, res)
}
})
})

server.on('error', ({ code }) => {
if (code in { EACCES:true, EADDRINUSE:true }) {
console.warn(`Port ${ports[0]} unavailable (${code})`)

if (ports.length > 1) {
ports.shift()
let p = ports[0]
console.warn(`Trying ${p? `port ${p}` : 'random port'}...`)
listen(p)
} else {
console.error('No more ports to try, giving up!')
process.exit(1)
}
}
})

server.on('listening', () => {
let { address, port } = server.address()
console.warn(`Serving at http://${address}:${port}`)

if (proxy) {
console.warn(`Proxying unmatched routes to ${proxy}`)
}

dns.reverse(address, (err, names) => {
let host = names? names[0] : address
names && names.map(name => console.warn(`Serving at http://${name}:${port}`))
open && opener(`http://${host}:${port}`)
})

})

let listen = $(server.listen.bind(server), $, address)

server.listen = () => listen(ports[0])

return server
}
30 changes: 30 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node

import argv from 'yargs'
import usage from './usage'
import { version } from '../package.json'
import { is } from 'funkis'
import createServer from './api'

let opts = argv
.version(version)
.options(
{ 'help' : { alias: 'h' }
, 'port' : { alias: 'p', string: true, default: 'random' }
, 'address' : { alias: 'a', string: true }
, 'dir' : { default: true, boolean: true, alias: 'showDir' }
, 'silent' : { alias: 's', boolean: true }
, 'open' : { alias: 'o', string: true }
, 'cache' : { alias: 'c', default: 0 }
, 'proxy' : { string: true }
, 'cors' : { default: true, boolean: true }
}
)
.argv

if (opts.help) {
usage()
process.exit()
} else {
createServer(opts).listen()
}
25 changes: 25 additions & 0 deletions src/getPorts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default getPorts

import { lowerCase, each, range, seq } from 'funkis'

function getPorts(ports) {
return (ports || 'random').split(',').reduce((parsed, p) => {
for (let [ptn, build] of patterns) {
let match = ptn.exec(p)
if (match) {
return parsed.concat(build(match))
}
}

throw new TypeError(`invalid port ${p}`)
}, [])
}

let patterns = new Map

patterns.set(/^(random|0)$/, _ => 0)
patterns.set(/^(\d+)-(\d+)$/, m => {
let [min, max] = [m[1] | 0, m[2] | 0]
return Array(max-min + 1).fill(min).map((_, i) => min + i)
})
patterns.set(/^\d+$/, ([port]) => [port | 0])
29 changes: 29 additions & 0 deletions src/usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { readFileSync as slurp } from 'fs'
import { resolve } from 'path'
import cliui from 'cliui'
import wsize from 'window-size'

let raw = slurp(resolve(__dirname, '../README.md'), 'utf8')
.split(/^\n*#+\s*/gim)
.reduce(function(usage, d) {
var p = d.split(/\n/)

if (p[0].match(/^usage$/i)) {
return p.slice(1).join('\n').replace(/^```.*$/gim, '').trim()
} else {
return usage
}
})

export default function usage() {
let wrap = Math.min(80, wsize.width || Infinity)

var ui = cliui({
width: wrap,
wrap: !!wrap
})

ui.div(raw)

console.error(ui.toString())
}