UNPKG

@sap/cds-dk

Version:

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

189 lines (158 loc) 5.33 kB
const cds = require('../cds'); const DEBUG = cds.debug('cli|watch|livereload'); module.exports = function livereload(opt) { // options opt = opt || {}; const ignore = opt.ignore || opt.excludeList || [/\.js(\?.*)?$/, /\.css(\?.*)?$/, /\.svg(\?.*)?$/, /\.ico(\?.*)?$/, /\.woff(\?.*)?$/, /\.png(\?.*)?$/, /\.jpg(\?.*)?$/, /\.jpeg(\?.*)?$/, /\.gif(\?.*)?$/, /\.pdf(\?.*)?$/, /\.json(\?.*)?$/ ]; const include = opt.include || [/.*/]; const html = opt.html || _html; const rules = opt.rules || [{ match: /<\/body>(?![\s\S]*<\/body>)/i, fn: prepend }, { match: /<\/html>(?![\s\S]*<\/html>)/i, fn: prepend }, { match: /<!DOCTYPE.+?>/i, fn: append }]; const disableCompression = opt.disableCompression || false; const port = opt.port || 35729; const plugins = opt.plugins || []; function snippet(host) { const src = opt.src || '//' + host + ':' + port + '/livereload.js?snipver=1'; return [src].concat(plugins).map(function(source) { return '<script src="' + source + '" async="" defer=""></script>'; }).join(''); } // helper functions const regex = (function () { const matches = rules.map(function (item) { return item.match.source; }).join('|'); return new RegExp(matches, 'i'); })(); function prepend(w, s) { return s + w; } function append(w, s) { return w + s; } function _html(str) { if (!str) return false; return /<[:_-\w\s!/="']+>/i.test(str); } function exists(body) { if (!body) return false; return regex.test(body); } function snip(body) { if (!body) return false; return (~body.lastIndexOf('/livereload.js')); } function snap(body, host) { let _body = body; rules.some(function (rule) { if (rule.match.test(body)) { _body = body.replace(rule.match, function(w) { return rule.fn(w, snippet(host)); }); return true; } return false; }); return _body; } function accept(req) { const ha = req.headers['accept']; if (!ha) return false; return (~ha.indexOf('html')); } function acceptContentType(res) { const ha = res.getHeader('content-type'); if (ha && ha.indexOf('html') >= 0) { DEBUG?.('[livereload] accepting content-type', ha) return true; } DEBUG?.('[livereload] rejecting content-type', ha) return false; } function check(str, arr) { if (!str) return true; return arr.some(function (item) { if ((item.test && item.test(str)) || ~str.indexOf(item)) return true; return false; }); } // middleware return function livereload(req, res, next) { const hostName = req.headers[':authority'] || req.headers.host; const host = opt.hostname || hostName.split(':')[0]; if (res._livereload) return next(); res._livereload = true; if (!accept(req) || !check(req.url, include) || check(req.url, ignore)) { return next(); } DEBUG?.('[livereload] accepting URL', req.url) // Disable G-Zip to enable proper inspecting of HTML if (disableCompression) { req.headers['accept-encoding'] = 'identity'; } let runPatches = () => acceptContentType(res); // const runPatches = () => true; const writeHead = res.writeHead; const write = res.write; const end = res.end; res.push = function (chunk) { res.data = (res.data || '') + chunk; }; res.inject = res.write = function (string, encoding) { if (!runPatches()) return write.call(res, string, encoding); if (string !== undefined) { const body = string instanceof Buffer ? string.toString(encoding) : string; // If this chunk must receive a snip, do so if (exists(body) && !snip(res.data)) { res.push(snap(body, host)); return true; } // If in doubt, simply buffer the data for later inspection (on `end` function) else { res.push(body); return true; } } return true; }; res.writeHead = function () { if (!runPatches()) return writeHead.apply(res, arguments); const headers = arguments[arguments.length - 1]; if (typeof headers === 'object') { for (const name in headers) { if (/content-length/i.test(name)) { delete headers[name]; } } } if (res.getHeader('content-length')) res.removeHeader('content-length'); writeHead.apply(res, arguments); }; res.end = function (string, encoding) { if (!runPatches()) return end.call(res, string, encoding); // If there are remaining bytes, save them as well // Also, some implementations call "end" directly with all data. res.inject(string, encoding); runPatches = () => false; // Check if our body is HTML, and if it does not already have the snippet. if (html(res.data) && exists(res.data) && !snip(res.data)) { // Include, if necessary, replacing the entire res.data with the included snippet. res.data = snap(res.data, host); } if (res.data !== undefined && !res._header) res.setHeader('content-length', Buffer.byteLength(res.data, encoding)); end.call(res, res.data, encoding); }; next(); }; };