simple-autoreload-server
Version:
Simple Web Server with live/autoreload features without browser extensions.
601 lines (599 loc) • 21.3 kB
JavaScript
/*
* simple-autoreload-server v0.2.15 - 2021-05-22
* <https://github.com/cytb/simple-autoreload-server>
*
* Copyright (c) 2021 cytb
*
* Licensed under the MIT License.
*/
(function(){
var http, https, fs, path, child_process, opener, connect, colors, morgan, serveIndex, serveStatic, WebSocket, parseurl, RecursiveWatcher, OptionHelper, Injecter, InjectionRouter, SimpleAutoreloadServer, slice$ = [].slice, this$ = this, out$ = typeof exports != 'undefined' && exports || this;
http = require('http');
https = require('https');
fs = require('fs');
path = require('path');
child_process = require('child_process');
opener = require('opener');
connect = require('connect');
colors = require('colors');
morgan = require('morgan');
serveIndex = require('serve-index');
serveStatic = require('serve-static');
WebSocket = require('faye-websocket');
parseurl = require('parseurl');
RecursiveWatcher = require('./watch').RecursiveWatcher;
OptionHelper = require('./option').OptionHelper;
Injecter = (function(){
Injecter.displayName = 'Injecter';
var prototype = Injecter.prototype, constructor = Injecter;
function Injecter(arg$){
var flag;
this.content = arg$.content, this.ignoreCase = arg$.ignoreCase, this.type = arg$.type, this.which = arg$.which, this.where = arg$.where, this.prepend = arg$.prepend, this.includeHidden = arg$.includeHidden, this.encoding = arg$.encoding;
this.fileMatcher = OptionHelper.readPattern(this.which, this.ignoreCase, this.includeHidden);
if (this.where instanceof RegExp) {
this.contentRegex = this.where;
} else {
flag = this.ignoreCase === false && "" || "i";
this.contentRegex = new RegExp(this.where, flag);
}
this.lengthOf = this.prepend && function(){
return 0;
} || function(it){
return it.length;
};
this.getCode = (function(){
var ref$;
switch (ref$ = [this.type], false) {
case "raw" !== ref$[0]:
return function(){
return this.content;
};
case "file" !== ref$[0]:
return this.constructor.getCachedLoader(process.cwd(), this.content, this.encoding);
default:
return function(){
return this.content;
};
}
}.call(this));
}
prototype.isTarget = function(it){
return this.fileMatcher(it);
};
prototype.matchContent = function(it){
return this.contentRegex.exec(it);
};
prototype.inject = function(file, text){
var m, pos;
if (this.isTarget(file) && (m = this.matchContent(text))) {
pos = m.index + this.lengthOf(m[0]);
text = text.slice(0, pos) + "" + this.getCode() + text.slice(pos);
}
return text;
};
Injecter.create = function(it){
return new Injecter(it);
};
Injecter.getCachedLoader = function(base, file, enc){
var last, p, cached;
enc == null && (enc = 'utf-8');
last = null;
p = path.resolve(base, file);
cached = "";
return function(){
var mtime;
mtime = fs.statSync(p).mtime;
if (last !== mtime) {
cached = fs.readFileSync(p, {
encoding: enc
});
cached = cached.toString();
last = mtime;
}
return cached;
};
};
return Injecter;
}());
InjectionRouter = (function(){
InjectionRouter.displayName = 'InjectionRouter';
var prototype = InjectionRouter.prototype, constructor = InjectionRouter;
function InjectionRouter(arg$){
this.path = arg$.path, this.target = arg$.target, this.inject = arg$.inject, this.defaultPages = arg$.defaultPages, this.encoding = arg$.encoding, this.listDirectory = arg$.listDirectory;
this.injecters = this.inject.map(bind$(Injecter, 'create'));
this.defaultPages = OptionHelper.readPattern(this.defaultPages);
}
prototype.route = function(req, res, next){
var ref$, url, rel, file, isDirpath, stat, dir, files, text, e;
if (!((ref$ = req.method) === 'GET' || ref$ === 'HEAD')) {
next();
}
url = parseurl(req);
rel = path.relative(this.target, url.pathname);
file = path.resolve(this.path, rel);
isDirpath = url.pathname.charAt(url.pathname.length - 1);
try {
stat = fs.statSync(file);
if (stat.isDirectory() && isDirpath) {
dir = file;
files = fs.readdirSync(dir).map(partialize$.apply(path, [path.join, [dir, void 8], [1]])).filter(function(name){
var ref$, e;
try {
return (ref$ = fs.statSync(name)) != null ? ref$.isFile() : void 8;
} catch (e$) {
e = e$;
return false;
}
}).sort().filter(bind$(this, 'defaultPages'));
if (files.length > 0) {
file = files[0];
stat = fs.statSync(file);
}
}
if (!(stat.isFile() && this.injecters.some(function(it){
return it.isTarget(file);
}))) {
return next();
}
text = fs.readFileSync(file, {
encoding: this.encoding
});
text = this.injecters.reduce(function(){
return arguments[1].inject(file, arguments[0]);
}, text);
res.setHeader("Content-Length", Buffer.byteLength(text));
res.setHeader("Content-Type", "text/html");
return res.end(text, this.encoding);
} catch (e$) {
e = e$;
return next();
}
};
return InjectionRouter;
}());
SimpleAutoreloadServer = (function(){
SimpleAutoreloadServer.displayName = 'SimpleAutoreloadServer';
var prototype = SimpleAutoreloadServer.prototype, constructor = SimpleAutoreloadServer;
SimpleAutoreloadServer.logPrefix = function(host){
return ("[autoreload #" + process.pid + " " + host + "]").cyan;
};
function SimpleAutoreloadServer(option){
option == null && (option = {});
this.websockets = [];
this.running = false;
this.setupOptions(option);
}
prototype.setupOptions = function(options, logger){
var optionHelper, getRootDef, defaults, defaultsSrc, key, names, base, ref$, i$, len$, def, assured, json, preFile, file, dir, nextDir, x$, newBase, y$, ref1$, out;
logger == null && (logger = function(){});
options = import$({}, options);
optionHelper = new OptionHelper;
getRootDef = function(pname, name, alter){
alter == null && (alter = name);
return function(options, defMap){
var that, ref$, ref1$, ref2$;
switch (false) {
case (that = (ref$ = options[pname]) != null ? ref$[name] : void 8) == null:
return that;
case !(that = (ref1$ = options[pname]) != null ? (ref2$ = ref1$[0]) != null ? ref2$[name] : void 8 : void 8):
return that;
case (that = options[name]) == null:
return that;
default:
return defMap[alter];
}
};
};
defaults = {};
defaultsSrc = {
mount: ['watch', 'recursive', 'followSymlinks', 'ignoreCase', 'includeHidden'],
inject: ['where', 'which', 'type', 'prepend', 'includeHidden']
};
for (key in defaultsSrc) {
names = defaultsSrc[key];
base = (ref$ = defaults[key]) != null
? ref$
: defaults[key] = {};
for (i$ = 0, len$ = names.length; i$ < len$; ++i$) {
def = names[i$];
base[def] = getRootDef(key, def);
}
}
assured = optionHelper.assure(options, defaults);
json = null;
preFile = file = null;
dir = null;
if (assured.searchConfig) {
nextDir = process.cwd();
do {
dir = nextDir;
preFile = file;
file = path.resolve(dir, assured.config);
try {
x$ = fs.readFileSync(file, {
encoding: this.encoding
});
json = JSON.parse(x$.toString());
} catch (e$) {}
nextDir = path.join(dir, "..");
} while (json == null && preFile !== file);
}
base = json != null
? json
: {};
if (json != null) {
this.basedir = path.resolve(dir, path.dirname(file));
process.chdir(this.basedir);
} else {
this.basedir = process.cwd();
}
newBase = import$(import$({}, base), options);
for (i$ = 0, len$ = (ref$ = ['inject', 'mount']).length; i$ < len$; ++i$) {
y$ = ref$[i$];
newBase[y$] = [].concat((ref1$ = base[y$]) != null
? ref1$
: [], (ref1$ = options[y$]) != null
? ref1$
: []);
}
out = optionHelper.assure(newBase, defaults);
if (out.inject == null || out.inject.length < 1) {
out.inject = [];
try {
file = path.resolve(this.basedir, '.autoreload.html');
if ((ref$ = fs.lstatSync(file)) != null && ref$.isFile()) {
out.inject.push({
content: file
});
}
} catch (e$) {}
out = optionHelper.assure(out, defaults);
}
if (!(out.onmessage instanceof Function)) {
out.onmessage = function(){};
}
this.options = out;
if (json != null) {
this.log("verbose", "options", "config loaded: " + file);
return this.log("verbose", "options", "change working directory to " + dir);
}
};
prototype.logLevel = [
{
level: 'silent',
tags: []
}, {
level: 'minimum',
tags: ['normal', 'error']
}, {
level: 'normal',
tags: ['normal', 'info', 'error']
}, {
level: 'verbose',
tags: ['normal', 'info', 'error', 'verbose']
}, {
level: 'noisy',
tags: ['normal', 'info', 'error', 'verbose', 'debug']
}
];
prototype.logLevelMap = {
silent: 0,
minimum: 1,
normal: 2,
verbose: 3,
noisy: 4
};
prototype.getLogLevel = function(level){
var i$, to$, i;
level == null && (level = "normal");
switch (false) {
case typeof level !== "boolean":
return level && 2 || 0;
case typeof level !== "string":
for (i$ = 0, to$ = this.logLevel.length; i$ < to$; ++i$) {
i = i$;
if (this.logLevel[i].level === level) {
return i;
}
}
for (i$ = 0, to$ = this.logLevel.length; i$ < to$; ++i$) {
i = i$;
if (i.toString() === level) {
return i;
}
}
return 2;
case !(0 <= level && level < this.logLevel.length):
return level;
default:
return 2;
}
};
prototype.log = function(mode, tag, text){
var level, coloredTag, prefix;
level = this.getLogLevel(this.options.log);
if (!in$(mode, this.logLevel[level].tags)) {
return;
}
coloredTag = (function(){
var ref$;
switch (ref$ = [mode], false) {
case 'error' !== ref$[0]:
return ("error@" + tag).red;
default:
return tag.green;
}
}());
prefix = this.constructor.logPrefix("localhost:" + this.options.port);
return console.log(prefix + " " + coloredTag + " " + text);
};
prototype.stop = function(){
var ref$, ref1$, ex;
try {
if ((ref$ = this.watcher) != null) {
ref$.stop();
}
if ((ref1$ = this.server) != null) {
ref1$.close();
}
this.running = false;
return this.log("normal", "server", "stopped.");
} catch (e$) {
ex = e$;
return this.log("error", "server", ex.message);
}
};
prototype.start = function(done){
var mounts, ref$, dirs, res$, i$, x$, len$, ex, listen, y$, this$ = this;
mounts = [];
try {
if (this.running) {
this.stop();
}
mounts = mounts.concat((ref$ = import$({}, this.options), ref$.target = "/", ref$));
mounts = mounts.concat((ref$ = this.options.mount) != null
? ref$
: []);
res$ = [];
for (i$ = 0, len$ = mounts.length; i$ < len$; ++i$) {
x$ = mounts[i$];
res$.push((ref$ = import$({}, x$), ref$.path = path.resolve(this.basedir, x$.path), ref$));
}
dirs = res$;
this.watchers = this.createWatchers(dirs);
this.server = this.createServer(dirs);
this.watchers.map(function(it){
return it.start();
});
} catch (e$) {
ex = e$;
this.log("error", "server", ex.message);
this.log("error", "server", ex.stack);
if (done != null) {
done(ex, this);
}
return null;
}
listen = {
port: (ref$ = this.options).port,
host: ref$.host,
path: ref$.path
};
y$ = this.server;
y$.on('upgrade', function(req, sock, head){
var addr, x$;
if (!WebSocket.isWebSocket(req)) {
return;
}
addr = sock.remoteAddress + ":" + sock.remotePort;
x$ = new WebSocket(req, sock, head);
x$.on('open', function(){
this$.log("verbose", 'websocket', addr + " - new connection");
return x$.send(JSON.stringify({
type: 'open',
log: this$.options.clientLog
}));
});
x$.on('message', function(arg$){
var data;
data = arg$.data;
this$.log("verbose", 'websocket', addr + " - received message " + data);
return this$.options.onmessage(data, x$);
});
x$.on('close', function(){
this$.log("verbose", 'websocket', addr + " - connection closed");
return this$.websockets = this$.websockets.filter((function(it){
return it !== x$;
}));
});
this$.websockets.push(x$);
return x$;
});
y$.on('error', function(err){
var ref$;
switch (ref$ = [err.code], false) {
case 'EADDRINUSE' !== ref$[0]:
this$.log("error", 'server', "Cannot use " + (listen.host + "").green + ":" + (listen.port + "").green + " as a listen address. Error: " + err.message + "");
return this$.watchers.map(function(it){
return it.stop();
});
}
});
y$.listen(listen.port | 0, listen.host, 511, function(){
var i$, x$, ref$, len$, that, y$, child, port, address, serverUrl;
this$.running = true;
this$.abspath = path.resolve(process.cwd(), listen.path) + "";
for (i$ = 0, len$ = (ref$ = mounts.slice(1)).length; i$ < len$; ++i$) {
x$ = ref$[i$];
this$.log("info", "server", "mounted " + x$.path + " to " + x$.target);
}
this$.log("normal", "server", "started on :" + (listen.port + "").green + " at " + this$.abspath);
if (that = this$.options.execute != null && this$.options.execute) {
this$.log("info", "server", "execute command: " + that);
y$ = child = child_process.exec(that, {
stdio: 'ignore'
});
y$.unref();
if (this$.options.stopOnExit) {
this$.log("info", "server", "server will stop when the command has exit.");
child.on('exit', function(){
this$.log("info", "server", "child command has finished.");
return this$.stop();
});
}
}
if (this$.options.browse) {
ref$ = this$.server.address(), port = ref$.port, address = ref$.address;
if (address === '0.0.0.0' || address === '::') {
address = "localhost";
}
serverUrl = (function(){
switch (typeof this.options.browse) {
case "string":
return this.options.browse;
default:
return "http://" + address + ":" + port + "/";
}
}.call(this$));
this$.log("info", "server", "open " + serverUrl);
opener(serverUrl, {
stdio: 'ignore'
}).unref();
}
if (done != null) {
return done(null, this$);
}
});
return y$;
};
prototype.createWatchers = function(dirs){
var shouldReload, watchObjs, this$ = this;
if (this.options.recursive) {
this.log("verbose", "server", "init with recursive-option. this may take a while.");
}
shouldReload = OptionHelper.readPattern(this.options.reload, this.options.ignoreCase, this.options.includeHidden);
watchObjs = dirs.map(function(dir){
var matcher;
matcher = OptionHelper.readPattern(dir.watch, dir.ignoreCase, dir.includeHidden);
return new RecursiveWatcher(import$(clone$(dir), {
delay: this$.options.watchDelay,
error: function(arg$, src){
var message;
message = arg$.message;
return this$.log("error", "watch", src + " Error: " + message);
},
update: function(arg$, target){
var httpPath, that;
if (!matcher(target)) {
return this$.log("debug", "watch", "(ignored)".cyan + " " + target);
} else {
this$.log("verbose", "watch", "updated " + target);
httpPath = '';
try {
if (that = /^[^/].*$/.exec(path.relative(dir.path, target))) {
httpPath = "/" + that[0];
}
} catch (e$) {}
return this$.broadcast({
type: 'update',
path: httpPath,
reload: shouldReload(httpPath)
});
}
}
}));
});
return watchObjs;
};
prototype.createServer = function(dirs){
var inject, encoding, i$, x$, ref$, len$, app, y$, opts, ref1$, ref2$;
inject = [].concat(this.options.inject);
encoding = this.options.encoding;
if (this.options.builtinScript) {
inject = inject.concat({
which: new RegExp(".*\\.html?$"),
where: new RegExp("</(body|head|html)>", "i"),
type: "file",
content: path.resolve(__dirname, '../client.html'),
prepend: true,
encoding: encoding
});
}
for (i$ = 0, len$ = (ref$ = [].concat(inject)).length; i$ < len$; ++i$) {
x$ = ref$[i$];
if (x$.type === "file") {
x$.content = path.resolve(this.basedir, x$.content);
}
}
app = (ref$ = this.options.connectApp) != null
? ref$
: connect();
if (this.getLogLevel(this.options.log) >= this.logLevel.verbose) {
app.use(morgan(':ar-prefix :remote-addr :method ":url HTTP/:http-version" :status :referrer :user-agent'));
}
for (i$ = 0, len$ = dirs.length; i$ < len$; ++i$) {
y$ = dirs[i$];
if (this.options.listDirectory) {
app.use(y$.target, serveIndex(y$.path, {
icons: true
}));
}
opts = (ref$ = (ref1$ = (ref2$ = {}, ref2$.path = y$.path, ref2$.target = y$.target, ref2$), ref1$.inject = inject, ref1$.encoding = encoding, ref1$), ref$.defaultPages = this.defaultPages, ref$);
app.use(y$.target, bind$(new InjectionRouter(opts), 'route'));
app.use(y$.target, serveStatic(y$.path));
}
return http.createServer(app);
};
prototype.broadcast = function(data){
var json, i$, x$, ref$, len$, ex, results$ = [];
try {
json = JSON.stringify(data);
this.log("debug", "broadcast", "to " + this.websockets.length + " sockets: " + json);
for (i$ = 0, len$ = (ref$ = this.websockets).length; i$ < len$; ++i$) {
x$ = ref$[i$];
results$.push(x$.send(json));
}
return results$;
} catch (e$) {
ex = e$;
return this.log("error", "broadcast", ex.message);
}
};
return SimpleAutoreloadServer;
}());
morgan.token('ar-prefix', function(r){
return (function(it){
return it + " httpd".green;
})(
SimpleAutoreloadServer.logPrefix(r.headers.host));
});
out$.SimpleAutoreloadServer = SimpleAutoreloadServer;
function bind$(obj, key, target){
return function(){ return (target || obj)[key].apply(obj, arguments) };
}
function partialize$(f, args, where){
var context = this;
return function(){
var params = slice$.call(arguments), i,
len = params.length, wlen = where.length,
ta = args ? args.concat() : [], tw = where ? where.concat() : [];
for(i = 0; i < len; ++i) { ta[tw[0]] = params[i]; tw.shift(); }
return len < wlen && len ?
partialize$.apply(context, [f, ta, tw]) : f.apply(context, ta);
};
}
function import$(obj, src){
var own = {}.hasOwnProperty;
for (var key in src) if (own.call(src, key)) obj[key] = src[key];
return obj;
}
function in$(x, xs){
var i = -1, l = xs.length >>> 0;
while (++i < l) if (x === xs[i]) return true;
return false;
}
function clone$(it){
function fun(){} fun.prototype = it;
return new fun;
}
}).call(this);