UNPKG

reload

Version:

Node.js module to refresh and reload your code in your browser when your code changes. No browser plugins required.

285 lines (245 loc) 10.2 kB
// Requires const path = require('path') const fs = require('fs') const http = require('http') const https = require('https') const ws = require('ws') module.exports = function reload (app, opts, server) { opts = opts || {} const argumentCache = arguments return new Promise(function (resolve, reject) { // Parameters variables const port = opts.port || 9856 const httpsOption = opts.https || null const httpServerOrPort = server || port const forceWss = opts.forceWss || false const verboseLogging = opts.verbose || false const webSocketServerWaitStart = opts.webSocketServerWaitStart || false // Application variables const RELOAD_FILE = path.join(__dirname, './reload-client.js') let reloadCode = fs.readFileSync(RELOAD_FILE, 'utf8') const route = opts.route ? processRoute(opts.route) : '/reload/reload.js' // Websocket server variables let wss // General variables const socketPortSpecified = server ? null : port const connections = {} let httpOrHttpsServer if (argumentCache[0] === undefined) { return reject(new Error('Lack of/invalid arguments provided to reload')) } else { if (typeof (argumentCache[0]) !== 'function' && (typeof (argumentCache[1]) !== 'object' || typeof (argumentCache[1]) !== 'undefined')) { return reject(new Error('Lack of/invalid arguments provided to reload')) } } if (typeof port !== 'number') { return reject(new Error('Specified port is not of type number')) } if (typeof forceWss !== 'boolean') { return reject(new Error('forceWss option specified is not of type boolean')) } if (typeof verboseLogging !== 'boolean') { return reject(new Error('verboseLogging option specified is not of type boolean')) } if (typeof webSocketServerWaitStart !== 'boolean') { return reject(new Error('webSocketServerWaitStart option specified is not of type boolean')) } // Application setup setupClientSideCode() setupExpressAppRouting().then(function () { if (!webSocketServerWaitStart) { startWebSocketServer().then(function (result) { resolve(result, 'test') }).catch(function (err) { reject(err) }) } else { resolve(getReloadReturn()) } }).catch(function (err) { return reject(err) }) function setupExpressAppRouting () { return new Promise(function (resolve, reject) { if (server === undefined) { if (app.get) { app.get(route, function (req, res) { res.type('text/javascript') res.send(reloadCode) }) resolve() } else { reject(new Error('Could not attach route to express app. Be sure that app passed is actually an express app')) } } else { resolve() } }) } function setupClientSideCode () { if (verboseLogging) { reloadCode = reloadCode.replace('verboseLogging = false', 'verboseLogging = true') } const webSocketString = forceWss ? 'wss://$3' : 'ws$2://$3' reloadCode = reloadCode.replace('socketUrl.replace()', 'socketUrl.replace(/(^http(s?):\\/\\/)(.*:)(.*)/,' + (socketPortSpecified ? '\'' + webSocketString + socketPortSpecified : '\'' + webSocketString + '$4') + '\')') } // Websocket server setup function startWebSocketServer () { const httpsOptions = {} const WebSocketServer = ws.Server return new Promise(function (resolve, reject) { if (verboseLogging) { console.log('Starting WebSocket Server') } if (socketPortSpecified) { // Use custom user specified port wss = new WebSocketServer({ noServer: true }) if (httpsOption) { // HTTPS if (httpsOption.p12) { if (typeof httpsOption.p12.p12Path === 'string' && httpsOption.p12.p12Path.match(/\.\w{3}$/)) { try { httpsOptions.pfx = fs.readFileSync(httpsOption.p12.p12Path) } catch (err) { return reject(err) } } else { httpsOptions.pfx = httpsOption.p12.p12Path } } else if (httpsOption.certAndKey) { /* istanbul ignore else */ if (httpsOption.certAndKey.key) { if (isCertString(httpsOption.certAndKey.key)) { httpsOptions.key = httpsOption.certAndKey.key } else { try { httpsOptions.key = fs.readFileSync(httpsOption.certAndKey.key) } catch (err) { return reject(err) } } } /* istanbul ignore else */ if (httpsOption.certAndKey.cert) { if (isCertString(httpsOption.certAndKey.cert)) { httpsOptions.cert = httpsOption.certAndKey.cert } else { try { httpsOptions.cert = fs.readFileSync(httpsOption.certAndKey.cert) } catch (err) { return reject(err) } } } } else { return reject(new Error('Could not initialize reload HTTPS setup incorrectly. Make sure to define a `p12` or `certAndKey` in the HTTPS options')) } /* istanbul ignore else */ if (httpsOption.passphrase) { httpsOptions.passphrase = httpsOption.passphrase } httpOrHttpsServer = https.createServer(httpsOptions) } else { // HTTP httpOrHttpsServer = http.createServer() } httpOrHttpsServer.listen(port, function () { resolve(getReloadReturn()) }) httpOrHttpsServer.on('upgrade', (request, socket, head) => { wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request) }) }) // Keep track of connections so we can force shutdown the server // https://stackoverflow.com/questions/14626636/how-do-i-shutdown-a-node-js-https-server-immediately/14636625#14636625 httpOrHttpsServer.on('connection', mapConnections) } else { // Attach to server, using server's port. Kept here to support legacy arguments. wss = new WebSocketServer({ server: httpServerOrPort }) resolve(getReloadReturn()) } wss.on('connection', (ws) => { if (verboseLogging) { console.log('Reload client connected to server') } }) }) } function sendMessage (message) { if (verboseLogging) { console.log('Sending message to ' + (wss.clients.size) + ' connection(s): ' + message) } wss.clients.forEach(function each (client) { /* istanbul ignore else */ if (client.readyState === ws.OPEN) { client.send(message) } }) } // assign individual keys to connections when opened so they can be destroyed gracefully function mapConnections (conn) { const key = conn.remoteAddress + ':' + conn.remotePort connections[key] = conn // once the connection closes, remove conn.on('close', function () { delete connections[key] }) } function processRoute (route) { // If reload.js is found in the route option strip it. We will concat it for user to ensure no case errors or order problems. const reloadJsMatch = route.match(/reload\.js/i) if (reloadJsMatch) { route = route.split(reloadJsMatch)[0] } /* * Concat their provided path (minus `reload.js` if they specified it) with a `/` if they didn't provide one and `reload.js. This allows for us to ensure case, order, and use of `/` is correct * For example these route's are all valid: * 1. `newRoutePath` -> Their route + `/` + reload.js * 2. `newRoutePath/` -> Their route + reload.js * 3. `newRoutePath/reload.js` -> (Strip reload.js above) so now: Their route + reload.js * 4. `newRoutePath/rEload.js` -> (Strip reload.js above) so now: Their route + reload.js * 5. `newRoutePathreload.js` -> (Strip reload.js above) so now: Their route + `/` + reload.js * 6. `newRoutePath/reload.js/rEload.js/... reload.js n number of times -> (Strip above removes all reload.js occurrences at the end of the specified route) so now: Their route + 'reload.js` */ return route + (route.slice(-1) === '/' ? '' : '/') + 'reload.js' } function getReloadReturn () { const tempObject = { reload: function () { sendMessage('reload') }, wss, closeServer: function () { return new Promise(function (resolve, reject) { // Loop through all connections and terminate them for immediate server shutdown for (const key in connections) { connections[key].destroy() } httpOrHttpsServer.close(resolve) }) } } // Only define the function and make it available if the WebSocket is waiting in the first place if (webSocketServerWaitStart) { tempObject.startWebSocketServer = startWebSocketServer } if (server) { // Private return API only used in command line version of reload tempObject.reloadClientCode = function () { return reloadCode } } return tempObject } }) function isCertString (stringToTest) { let testString = stringToTest if (typeof testString !== 'string') { testString = testString.toString() } const lastChar = testString.substring(testString.length - 1) // A file path string won't have an end of line character at the end // Looking for either \n or \r allows for nearly any OS someone could // use, and a few that node doesn't work on. if (lastChar === '\n' || lastChar === '\r') { return true } return false } }