UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

162 lines (144 loc) 5.67 kB
const cds = require('../cds') const LOG = cds.log('cli|watch|livereload'), DEBUG = cds.debug('cli|watch|livereload') const axios = require('axios') const ws = require('ws') const express = require('express') const { URL } = require('url') const { relative, sep } = require ('path') const ext = 'css,gif,html,jpg,png,svg,ts,mjs,cjs,js,json,properties,change,variant,ctrl_variant,ctrl_variant_change,ctrl_variant_management_change' const watchIncludes = RegExp(`\\.(${ext.replace(/,/g,'|')})$|\\${sep}(webapp|appconfig)`) const watchExcludes = RegExp(`@cds-models\\${sep}|@types\\${sep}`) module.exports = class LiveReload { async start(port, cwd, delay=0) { if (!parseInt(port)) port = 35729 this.files = [] // install the JS script for the client const app = express() app.use('/livereload.js', express.static(require.resolve('livereload-js'))) return new Promise ((resolve, reject) => { let server const listening = async () => { const {address,port} = server.address() startWs(server) this.url = await livereloadURL(address, port) DEBUG?.(`live reload available at ${this.url}`) resolve(this.url) } // start server server = app.listen (port,'localhost') server.once('listening', listening) server.on('error', e => { // try once more w/ ephemeral port 0 if (e.code === 'EADDRINUSE') { server = app.listen (0,'localhost') server.once('listening', listening) server.on('error', reject) } else reject(e) }) // start websocket const startWs = (server) => { this.wsServer = new ws.Server({ server }) this.wsServer.on('connection', (socket) => { DEBUG?.('Client connected') socket.on('message', (msg) => { DEBUG?.('Client message', msg.toString()) const request = JSON.parse(msg) if (request.command === 'hello') { DEBUG?.('Client handshake') const data = JSON.stringify({ command: 'hello', protocols: [ 'http://livereload.com/protocols/official-7', 'http://livereload.com/protocols/official-8', 'http://livereload.com/protocols/official-9', 'http://livereload.com/protocols/2.x-origin-version-negotiation', 'http://livereload.com/protocols/2.x-remote-control', ], serverName: 'cds-watch', }) socket.send(data) } else if (request.command === 'info') { DEBUG?.('Server received client data. Not sending response.') } }) }) if (LOG._debug) { server.on('close', () => DEBUG('⚡️','cds watch - livereload closed')) this.wsServer.on('close', () => DEBUG('⚡️','cds watch - web socket closed')) this.wsServer.on('error', (e) => DEBUG(e)) } } process.once('shutdown', ()=>{ this.wsServer.clients?.forEach(ws => ws.terminate()) this.wsServer.close() server.close() }) // Watching for touched files... const watch = require('node-watch') const filter = f => { const isIn = !watchExcludes.test(f) && watchIncludes.test(f) if (!isIn) DEBUG?. (`livereload ignored: ${relative(cwd||process.cwd(), f)}`) // else DEBUG?. (`livereload NOT ignored: ${relative(cwd||process.cwd(), f)}`, watchIncludes) return isIn } const watcher = watch (cwd||process.cwd(),{ recursive:true, filter, delay:0 }) if (LOG._debug) watcher.on('close', ()=> DEBUG('⚡️', 'cds watch - livereload file watcher closed')) watcher.on('change', _coalesceEvents(delay, (files) => { DEBUG?. (`livereload for: ${files.map(f => f.name)}`) this.markPending(files.map(f => f.name)) this.reload() })) process.once('shutdown', ()=> watcher.close()) }) } markPending(files=[]) { this.files = files } reload() { const data = JSON.stringify({ command: 'reload', path: this.files.length ? this.files[0] : '', }) DEBUG?.(`live reload for ${this.files}. ${this.wsServer.clients.size} ws clients`) broadcast(data, this.wsServer.clients) this.files = [] } get address() { return this.url ? new URL(this.url).host : null } } // reduces mutiple node-watch events in a time frame into a bulk function _coalesceEvents(delay, fn) { let timer, cache = [] return function(type, name) { // node-watch callback signature if (type) cache.push({type, name}) if (!timer) timer = setTimeout(() => { fn(cache) timer = null cache = [] }, delay).unref(); } } function broadcast(data, sockets) { sockets.forEach(socket => { DEBUG?.(`Sending ${data}`) socket.send(data, err=> DEBUG?.(err)) }) } async function livereloadURL(address, port) { if (process.env.WS_BASE_URL) { // on BAS, expose the livereload port as an externally accessible URL try { const resp = await axios.get('http://localhost:3001/AppStudio/api/getHostByPort?port=' + port) return resp.data.result + 'livereload.js?snipver=1&port=443' } catch (e) { console.error(`Error exposing live reload port ${port}: ${e.message}`) return } } // Local case. Add effective server address/host to URL. // Make sure IPv6 addresses are properly escaped. if (require('net').isIPv6(address)) address = `[${address}]` return `http://${address}:${port}/livereload.js?snipver=1` }