five-server
Version:
Development Server with Live Reload Capability. (Maintained Fork of Live Server)
621 lines (618 loc) • 26.3 kB
JavaScript
;
/* eslint-disable sort-imports */
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
* @copyright
* Copyright (c) 2012 Tapio Vierros (https://github.com/tapio)
* Copyright (c) 2021 Yannick Deubel (https://github.com/yandeu)
*
* @license {@link https://github.com/yandeu/five-server/blob/main/LICENSE LICENSE}
*
* @description
* forked from live-server@1.2.1 (https://github.com/tapio/live-server)
* previously licensed under MIT (https://github.com/tapio/live-server#license)
*/
const chokidar_1 = __importDefault(require("chokidar"));
const misc_1 = require("./misc");
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const express6_1 = require("express6");
const ws_1 = require("ws");
// some imports
const colors_1 = require("./colors");
const getCertificate_1 = require("./utils/getCertificate");
const getNetworkAddress_1 = require("./utils/getNetworkAddress");
const openBrowser_1 = require("./openBrowser");
// execute php
const execPHP_1 = require("./utils/execPHP");
const public_1 = require("./public");
const msg_1 = require("./msg");
const PHP = new execPHP_1.ExecPHP();
// for hot body injections
const workerPool_1 = __importDefault(require("./workers/workerPool"));
// middleware
const explorer_1 = __importDefault(require("./middleware/explorer")); // const serveIndex = require('serve-index')
const findIndex_1 = require("./middleware/findIndex");
const injectCode_1 = require("./middleware/injectCode");
const fallbackFile_1 = require("./middleware/fallbackFile");
const preview_1 = require("./middleware/preview");
const ignoreExtension_1 = require("./middleware/ignoreExtension");
const favicon_1 = require("./middleware/favicon");
const notFound_1 = require("./middleware/notFound");
const cache_1 = require("./middleware/cache");
class LiveServer {
constructor() {
this.logLevel = 1;
this.injectBody = false;
/** Absolute path of workspace or process.cwd() */
this.cwd = '';
/** inject stript to any file */
this.servePreview = true;
this._parseBody_IsValidHtml = true;
this.colors = ['magenta', 'cyan', 'blue', 'green', 'yellow', 'red'];
this.colorIndex = -1;
this.newColor = () => {
this.colorIndex++;
return this.colors[this.colorIndex % this.colors.length];
};
/** WebSocket Clients Array */
this.wsc = [];
// http sockets
this.sockets = new Set();
this.ipColors = new Map();
}
_parseBody_updateBody(fileName, text, shouldHighlight = false, cursorPosition = { line: 0, character: 0 }) {
var _a;
(_a = this._parseBody) === null || _a === void 0 ? void 0 : _a.postMessage(JSON.stringify({
fileName,
text,
shouldHighlight,
cursorPosition
}));
}
get parseBody() {
if (this._parseBody)
return { workers: this._parseBody, updateBody: this._parseBody_updateBody.bind(this) };
this._parseBody = new workerPool_1.default('./parseBody.js', {
worker: 2,
rateLimit: 50,
logLevel: this.logLevel,
init: { phpExecPath: PHP.path, phpIniPath: PHP.ini, cwd: this.cwd }
});
this._parseBody.on('message', d => {
const data = JSON.parse(d);
if (data.ignore)
return;
if (data.time)
console.log('TIME', data.time);
const { body, report, fileName } = data;
if (report.valid) {
this.updateBody(fileName, body);
if (!this._parseBody_IsValidHtml) {
this._parseBody_IsValidHtml = true;
this === null || this === void 0 ? void 0 : this.sendMessage(fileName, 'HIDE_MESSAGES');
}
}
else {
this._parseBody_IsValidHtml = false;
if (report.results.length > 0 && report.results[0].messages.length > 0) {
const errors = report.results[0].messages.map(m => `${m.message}\n at line: ${m.line}`);
this.sendMessage(fileName, errors);
}
}
});
return { workers: this._parseBody, updateBody: this._parseBody_updateBody.bind(this) };
}
get openURL() {
return this._openURL;
}
get protocol() {
return this._protocol;
}
get isRunning() {
var _a;
return !!((_a = this.httpServer) === null || _a === void 0 ? void 0 : _a.listening);
}
/** Start five-server */
async start(options = {}) {
if (!options._cli) {
const opts = (0, misc_1.getConfigFile)(options.configFile, options.workspace);
options = { ...opts, ...options };
}
const { browser = 'default', cache = true, cors = true, file, htpasswd = null, https: _https = null, injectBody = false, injectCss = true, logLevel = 1, middleware = [], mount = {}, php, phpIni, port = 5500, proxy = {}, remoteLogs = false, baseURL = '/', useLocalIp = false, wait = 100, withExtension = 'unset', workspace } = options;
PHP.path = php;
PHP.ini = phpIni;
this.logLevel = msg_1.message.logLevel = logLevel;
let host = options.host || '0.0.0.0'; // 'localhost'
if (useLocalIp && host === 'localhost')
host = '0.0.0.0';
this.injectBody = injectBody;
let _watch = options.watch;
if (_watch === true)
_watch = ['.'];
if (_watch === false)
_watch = false;
else if (_watch && !Array.isArray(_watch))
_watch = [_watch];
if (typeof _watch === 'undefined')
_watch = ['.'];
// tmp
const _tmp = options.root || process.cwd();
/** CWD (absolute path) */
this.cwd = workspace ? workspace : process.cwd();
/** root (absolute path) */
const root = workspace ? path_1.default.join(workspace, options.root ? options.root : '') : path_1.default.resolve(_tmp);
/** file, dir, glob, or array => passed to chokidar.watch() (relative path to CWD) */
const watch = _watch;
// console.log('cwd', cwd)
// console.log('root', root)
// console.log('watch', watch)
/** the path(s) to be opened in the browser */
let openPath = options.open;
if (typeof openPath === 'string')
openPath = (0, misc_1.removeLeadingSlash)(openPath);
else if (Array.isArray(openPath))
openPath.map(o => (0, misc_1.removeLeadingSlash)(o));
else if (openPath === undefined || openPath === true)
openPath = '';
else if (openPath === null || openPath === false)
openPath = null;
// replace \ by /
if (typeof openPath === 'string')
openPath = openPath.replace(/\\/gm, '/');
else if (Array.isArray(openPath)) {
openPath.map(p => p.replace(/\\/gm, '/'));
}
if (options.noBrowser)
openPath = null; // Backwards compatibility
// if server is already running, just open a new browser window
if (this.isRunning) {
const printOpenNewMessage = (openPath) => {
const path = (0, colors_1.colors)(openPath, 'bold');
const url = (0, colors_1.colors)(`${this.openURL}/${path}`, 'cyan');
msg_1.message.log(`Opening new window at ${url}`);
};
if (openPath === null) {
msg_1.message.log(`Can't open new window for path "null"`);
return;
}
if (Array.isArray(openPath)) {
openPath.forEach(p => {
printOpenNewMessage(p);
});
}
else {
printOpenNewMessage(openPath);
}
this.launchBrowser(this.openURL, openPath, browser);
return;
}
/**
* deprecation notice
*/
// Use http-auth if configured
if (htpasswd !== null)
msg_1.message.error('Sorry htpasswd does not work yet.', null, false);
// Custom https module
if (options.httpsModule)
msg_1.message.error('Sorry "httpsModule" has been removed.', null, false);
// SPA middleware
if (options.spa)
msg_1.message.error('Sorry SPA middleware has been removed.', null, false);
/**
* STEP: 1/4
* Set up "express" server (https://www.npmjs.com/package/express)
*/
// express.js
const app = (0, express6_1.express)();
// change x-powered-by
app.use((req, res, next) => {
res.setHeader('X-Powered-By', 'Five Server');
next();
});
// enable CORS
if (cors)
app.use(require('cors')({ credentials: true }));
// .cache
if (cache)
app.use('/.cache', cache_1.cache);
// serve fiveserver files
app.use((req, res, next) => {
if (req.url === '/fiveserver.js')
return res.type('.js').send(public_1.INJECTED_CODE);
if (req.url === '/fiveserver/status')
return res.json({ status: 'online' });
next();
});
// fiveserver /public
app.use('/fiveserver', (0, express6_1.Static)(path_1.default.join(__dirname, '../public')));
/*
// logger has been removed from the core
// if you want a logger, add a custom middleware to fiveserver.config.js
const morgan = require('morgan')
module.exports = {
middleware: [morgan('dev')]
}
module.exports = {
middleware: [morgan('dev', { skip: (req, res) => res.statusCode < 400 })]
}
*/
// middleware
middleware.map(function (mw) {
if (typeof mw === 'string') {
const ext = path_1.default.extname(mw).toLocaleLowerCase();
if (ext !== '.js') {
mw = require(path_1.default.join(__dirname, 'middleware', `${mw}.js`)).default;
}
else {
mw = require(mw);
}
if (typeof mw !== 'function')
msg_1.message.error(`middleware ${mw} does not return a function`, null, false);
}
app.use(mw);
});
// mount
for (const [ROUTE, TARGET] of Object.entries(mount)) {
const mountPath = path_1.default.resolve(process.cwd(), TARGET);
let R = ROUTE;
// automatically watch mount paths
if (!options.watch && watch !== false)
watch.push(mountPath);
// make sure ROUTE has a leading slash
if (R.indexOf('/') !== 0)
R = `/${R}`;
// inject code to html and php files
app.use(R, (0, injectCode_1.injectCode)(mountPath, baseURL, PHP, injectBody || false));
// serve static files via express.static()
app.use(R, (0, express6_1.Static)(mountPath));
// log the mapping folder
if (this.logLevel >= 1)
msg_1.message.log(`Mapping "${R}" to "${TARGET}"`);
}
// proxy
for (const [ROUTE, TARGET] of Object.entries(proxy)) {
const url = new URL(TARGET);
const proxyOpts = {
hash: url.hash,
host: url.host,
hostname: url.hostname,
href: url.href,
origin: url.origin,
password: url.password,
path: url.pathname + url.search,
pathname: url.pathname,
port: url.port,
preserveHost: true,
protocol: url.protocol,
search: url.search,
searchParams: url.searchParams,
username: url.username,
via: true
};
const { proxyMiddleware } = require('./middleware/proxy');
app.use(ROUTE, proxyMiddleware(proxyOpts, baseURL, injectBody || false));
if (this.logLevel >= 1)
msg_1.message.log(`Mapping "${ROUTE}" to "${TARGET}"`);
}
// find index file and modify req.url
app.use((0, findIndex_1.findIndex)(root, withExtension, ['html', 'php']));
const injectHandler = (0, injectCode_1.injectCode)(root, baseURL, PHP, injectBody || false);
// inject five-server script
app.use(injectHandler);
// serve static files (ignore php files) (don't serve index files)
app.use((0, ignoreExtension_1.ignoreExtension)(['php'], (0, express6_1.Static)(root, { index: false })));
// inject to fallback "file"
app.use((0, fallbackFile_1.fallbackFile)(injectHandler, file));
// inject to any (converts and file to a .html file (if possible))
// (makes that nice preview page)
app.use((0, preview_1.preview)(root, baseURL, this.servePreview));
// explorer middleware (previously serve-index)
app.use((0, explorer_1.default)(root, { icons: true, hidden: false, dotFiles: true, baseURL: baseURL }));
// no one want to see a 404 favicon error
app.use(favicon_1.favicon);
// serve 403/404 page
app.use((0, notFound_1.notFound)(root, baseURL));
// create http server
if (_https !== null && _https !== false) {
let httpsConfig = _https;
if (typeof _https === 'string') {
httpsConfig = require(path_1.default.resolve(process.cwd(), _https));
}
if (_https === true) {
const fakeCert = (0, getCertificate_1.getCertificate)(path_1.default.join(workspace ? workspace : path_1.default.resolve(), '.cache'));
httpsConfig = { key: fakeCert, cert: fakeCert };
}
this.httpServer = https_1.default.createServer(httpsConfig, app);
this._protocol = 'https';
}
else {
this.httpServer = http_1.default.createServer(app);
this._protocol = 'http';
}
// start and listen at port
await this.listen(port, host);
const address = this.httpServer.address();
//const serveHost = address.address === '0.0.0.0' ? '127.0.0.1' : address.address
let openHost = host === '0.0.0.0' ? '127.0.0.1' : host;
if (useLocalIp)
openHost = (0, getNetworkAddress_1.getNetworkAddress)() || openHost;
//const serveURL = `${this._protocol}://${serveHost}:${address.port}`
this._openURL = `${this._protocol}://${openHost}:${address.port}`;
msg_1.message.log('');
msg_1.message.log(` Five Server ${(0, colors_1.colors)('running at:', 'green')}`);
// message.log(colors(` (v${VERSION} http://npmjs.com/five-server)`, 'gray'))
msg_1.message.log('');
//let serveURLs: any = [serveURL]
if (this.logLevel >= 1) {
if (address.address === '0.0.0.0') {
const interfaces = os_1.default.networkInterfaces();
Object.keys(interfaces).forEach(key => (interfaces[key] || [])
.filter(details => details.family === 'IPv4')
.map(detail => {
return {
type: detail.address.includes('127.0.0.1') ? 'Local: ' : 'Network: ',
host: detail.address.replace('127.0.0.1', 'localhost')
};
})
.forEach(({ type, host }) => {
const url = `${this._protocol}://${host}:${(0, colors_1.colors)(address.port, 'bold')}`;
msg_1.message.log(` > ${type} ${(0, colors_1.colors)(url, 'cyan')}`);
}));
}
else {
msg_1.message.log(` > Local: ${(0, colors_1.colors)(`${this._protocol}://${openHost}:${(0, colors_1.colors)(address.port, 'bold')}`, 'cyan')}`);
}
msg_1.message.log('');
}
// donate
(0, misc_1.donate)();
/**
* STEP: 2/4
* Open Browser using "open" (https://www.npmjs.com/package/open)
*/
this.launchBrowser(this.openURL, openPath, browser);
/**
* STEP: 3/4
* Make WebSocket Connection using "ws" (https://www.npmjs.com/package/ws)
*/
this.wss = new ws_1.WebSocketServer({ server: this.httpServer });
// keep track of delayed file changes
let totalChanges = 0;
let lastChange = new Date().getTime();
this.wss.broadcastWithDelay = (wsc, data) => {
totalChanges++;
setTimeout(() => {
totalChanges--;
if (totalChanges === 0) {
// send immediately
wsc.forEach(ws => ws.send(data, e => { }));
}
else {
// send with rate limit
const now = new Date().getTime();
if (now - lastChange > wait) {
lastChange = now;
wsc.forEach(ws => ws.send(data, e => { }));
}
}
}, wait, { once: true });
};
this.wss.on('connection', (ws, req) => {
var _a;
if (remoteLogs !== false)
ws.send('initRemoteLogs');
ws.send('connected');
// store ip
ws.ip = (_a = req === null || req === void 0 ? void 0 : req.connection) === null || _a === void 0 ? void 0 : _a.remoteAddress;
// store color
const clr = this.ipColors.get(ws.ip) || this.newColor();
this.ipColors.set(ws.ip, clr);
ws.color = clr;
// ws.on('error', err => {
// message.log('WS ERROR:', err)
// })
ws.on('message', (_data, isBinary) => {
try {
// see: https://github.com/websockets/ws/releases/tag/8.0.0
const data = isBinary ? _data : _data.toString();
if (typeof data === 'string') {
const json = JSON.parse(data);
if (json && json.file) {
ws.file = json.file;
}
const useRemoteLogs = remoteLogs === true || typeof remoteLogs === 'string';
if (useRemoteLogs && json && json.console) {
const ip = `[${ws.ip}]`;
const msg = json.console.message;
const T = json.console.type;
const clr = T === 'warn' ? 'yellow' : T === 'error' ? 'red' : '';
const log = `${(0, colors_1.colors)(ip, ws.color)} ${clr ? (0, colors_1.colors)(msg, clr) : msg}`;
msg_1.message.pretty(log, { id: 'ws' });
}
}
}
catch (err) {
//
}
});
ws.on('close', () => {
this.wsc = this.wsc.filter(function (x) {
return x !== ws;
});
});
this.wsc.push(ws);
});
/**
* STEP: 4/4
* Listen for File changes using "chokidar" (https://www.npmjs.com/package/chokidar)
*/
let ignored = [
function (testPath) {
// Always ignore dotfiles (important e.g. because editor hidden temp files)
return testPath !== '.' && /(^[.#]|(?:__|~)$)/.test(path_1.default.basename(testPath));
},
/(^|[/\\])\../, // ignore dotfile
'**/node_modules/**',
'**/bower_components/**',
'**/jspm_packages/**'
];
if (options.ignore) {
ignored = ignored.concat(options.ignore);
}
if (options.ignorePattern) {
ignored.push(options.ignorePattern);
}
// Setup file watcher
if (watch === false)
return;
this.watcher = chokidar_1.default.watch(watch, {
cwd: this.cwd,
ignoreInitial: true,
ignored: ignored
});
const handleChange = changePath => {
const cssChange = path_1.default.extname(changePath) === '.css' && injectCss;
if (this.logLevel >= 1) {
const five = (0, colors_1.colors)((0, colors_1.colors)('[Five Server]', 'bold'), 'cyan');
const msg = cssChange ? (0, colors_1.colors)('CSS change detected', 'magenta') : (0, colors_1.colors)('change detected', 'cyan');
const file = (0, colors_1.colors)(changePath.replace(path_1.default.resolve(root), ''), 'gray');
msg_1.message.pretty(`${five} ${msg} ${file}`, { id: cssChange ? 'cssChange' : 'change' });
}
const htmlChange = path_1.default.extname(changePath) === '.html';
const phpChange = path_1.default.extname(changePath) === '.php';
if ((htmlChange || phpChange) && injectBody)
return;
this.wss.broadcastWithDelay(this.wsc, cssChange ? 'refreshcss' : 'reload');
};
this.watcher
.on('change', handleChange)
.on('add', handleChange)
.on('unlink', handleChange)
.on('addDir', handleChange)
.on('unlinkDir', handleChange)
.on('ready', () => {
if (this.logLevel > 1)
msg_1.message.log((0, colors_1.colors)('Ready for changes', 'cyan'));
})
.on('error', err => {
msg_1.message.log((0, colors_1.colors)('ERROR:', 'red'), err);
});
}
async listen(port, host) {
return new Promise((resolve, reject) => {
// Handle server startup errors
this.httpServer.once('error', e => {
// @ts-ignore
if (e.message === 'EADDRINUSE' || (e.code && e.code === 'EADDRINUSE')) {
// const serveURL = `${this._protocol}://${host}:${port}`
msg_1.message.log((0, colors_1.colors)(`Port ${port} is already in use. Trying another port.`, 'yellow'));
setTimeout(() => {
this.listen(0, host); // 0 means random port
}, 1000);
}
else {
msg_1.message.error((0, colors_1.colors)(e.toString(), 'red'), null, false);
this.shutdown();
reject(e.message);
}
});
this.httpServer.on('connection', socket => {
this.sockets.add(socket);
});
// Handle successful httpServer
this.httpServer.once('listening', ( /*e*/) => {
resolve();
});
this.httpServer.listen(port, host);
});
}
/**
* Navigate the browser to another page.
* @param url Navigates to the given URL.
*/
navigate(url) {
this.wss.broadcastWithDelay(this.wsc, JSON.stringify({ navigate: url }));
}
/** Launch a new browser window. */
async launchBrowser(openURL, path, browser) {
await (0, openBrowser_1.openBrowser)(openURL, path, browser);
}
/** Reloads all browser windows */
reloadBrowserWindow() {
this.wss.broadcastWithDelay(this.wsc, 'reload');
}
/** Send message to the client. (Will show a popup in the Browser) */
sendMessage(file, msg, type = 'info') {
this.wsc.forEach(ws => {
// send message or message[s]
const content = typeof msg === 'string' ? { message: msg } : { messages: msg };
if (ws && ws.file === decodeURI(file))
ws.send(JSON.stringify(content));
});
}
/** Manually refresh css */
refreshCSS(showPopup = true) {
this.wss.broadcastWithDelay(this.wsc, showPopup ? 'refreshcss' : 'refreshcss-silent');
}
/** Inject a a new <body> into the DOM. (Better prepend parseBody first) */
updateBody(file, body) {
this.wsc.forEach(ws => {
if (ws && ws.file === decodeURI(file))
ws.send(JSON.stringify({ body, hot: true }));
});
}
/** @deprecated */
highlightSelector(file, selector) {
// TODO(yandeu): add this
}
/** @deprecated */
highlight(file, position) {
// this.wsc.forEach(ws => {
// if (ws && ws.file === decodeURI(file)) ws.sendWithDelay(JSON.stringify({ position }))
// })
}
/** Close five-server (same as shutdown()) */
get close() {
return this.shutdown;
}
/** Shutdown five-server */
async shutdown() {
var _a;
await ((_a = this._parseBody) === null || _a === void 0 ? void 0 : _a.terminate());
if (this.watcher) {
await this.watcher.close();
}
this.wss.close();
for (const ws of this.wsc) {
ws.terminate();
}
for (const socket of this.sockets) {
socket.destroy();
this.sockets.delete(socket);
}
return new Promise((resolve, reject) => {
if (this.httpServer && this.httpServer.listening) {
this.httpServer.close(err => {
if (err)
return reject(err.message);
else {
resolve();
// @ts-ignore
this.httpServer = null;
}
});
}
else {
return resolve();
}
});
}
}
exports.default = LiveServer;
//# sourceMappingURL=index.js.map