@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
189 lines (158 loc) • 5.33 kB
JavaScript
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();
};
};