UNPKG

five-server

Version:

Development Server with Live Reload Capability. (Maintained Fork of Live Server)

621 lines (618 loc) 26.4 kB
"use strict"; /* 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 express_1 = __importDefault(require("express")); 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, express_1.default)(); // 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', express_1.default.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, express_1.default.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'], express_1.default.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