UNPKG

simple-autoreload-server

Version:

Simple Web Server with live/autoreload features without browser extensions.

601 lines (599 loc) 21.3 kB
/* * 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);