Mercurial > piecrust2
diff piecrust/resources/server/piecrust-debug-info.js @ 552:9612cfc6455a
serve: Rewrite of the Server-Sent Event code for build notifications.
At the moment the server monitors the asset directories, and notifies the
browser when an asset has changed and has been re-processed.
* Fix issues around long-running requests/threads which mess up the ability
to shutdown the server correctly with `CTRL-C` (see comments in code).
* Move the notification queue to each SSE producer, to support having multiple
pages open in a browser.
* Add JS/CSS for showing quick notifications about re-processed assets.
* Add support for hot-reloading CSS and pictures that have been re-processed.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sat, 08 Aug 2015 16:12:04 -0700 |
parents | d7a548ebcd58 |
children | 93b656f0af54 |
line wrap: on
line diff
--- a/piecrust/resources/server/piecrust-debug-info.js Sat Aug 08 15:55:24 2015 -0700 +++ b/piecrust/resources/server/piecrust-debug-info.js Sat Aug 08 16:12:04 2015 -0700 @@ -1,45 +1,284 @@ +/////////////////////////////////////////////////////////////////////////////// +// PieCrust debug info and features +// +// This stuff is injected by PieCrust's preview server and shouldn't show up +// in production. It should all be self-contained in this one file. +/////////////////////////////////////////////////////////////////////////////// + var eventSource = new EventSource("/__piecrust_debug/pipeline_status"); -//window.onbeforeunload = function(e) { -// console.log("Disconnecting SSE.", e); -// eventSource.close(); -//}; +if (eventSource != null) { + eventSource.onerror = function(e) { + console.log("Error with SSE, closing.", e); + eventSource.close(); + }; + + eventSource.addEventListener('ping', function(e) { + }); + + eventSource.addEventListener('pipeline_success', function(e) { + var obj = JSON.parse(e.data); + console.log("Got pipeline success", obj); + + // Check which assets were processed, and whether they are referenced + // by the current page for the usual use-cases. + for (var i = 0; i < obj.assets.length; ++i) { + a = obj.assets[i]; + if (assetReloader.reloadAsset(a)) { + notification.flashSuccess("Reloaded " + a); + } + } + }); + + eventSource.addEventListener('pipeline_error', function(e) { + var obj = JSON.parse(e.data); + console.log("Got pipeline error", obj); + + var outer = document.createElement('div'); + outer.style = 'padding: 1em;'; + for (var i = 0; i < obj.assets.length; ++i) { + var item = obj.assets[i]; + var markup = ( + '<p>Error processing: <span style="font-family: monospace;">' + + item.path + '</span></p>\n' + + '<ul>'); + for (var j = 0; j < item.errors.length; ++j) { + markup += ( + '<li style="font-family: monospace;">' + + item.errors[j] + + '</li>\n'); + } + markup += '</ul>\n'; + var entry = document.createElement('div'); + entry.innerHTML = markup; + outer.appendChild(entry); + } + + var placeholder = document.getElementById('piecrust-debug-info-pipeline-status'); + placeholder.appendChild(outer); + }); +} + +/////////////////////////////////////////////////////////////////////////////// + +NotificationArea = function() { + var area = document.createElement('div'); + area.id = 'piecrust-debug-notifications'; + area.className = 'piecrust-debug-notifications'; + document.querySelector('body').appendChild(area); + + this._area = area; + this._lastId = 0; +}; -eventSource.onerror = function(e) { - console.log("Error with SSE, closing.", e); - eventSource.close(); +NotificationArea.prototype.flashSuccess = function(msg) { + this.flashMessage(msg, 'success'); +}; + +NotificationArea.prototype.flashError = function(msg) { + this.flashMessage(msg, 'error'); +}; + +NotificationArea.prototype.flashMessage = function(msg, css_class) { + this._lastId += 1; + var thisId = this._lastId; + this._area.insertAdjacentHTML( + 'afterbegin', + '<div id="piecrust-debug-notification-msg' + thisId + '" ' + + 'class="piecrust-debug-notification ' + + 'piecrust-debug-notification-' + css_class + '">' + + msg + '</div>'); + + window.setTimeout(this._discardNotification, 2000, thisId); +}; + +NotificationArea.prototype._discardNotification = function(noteId) { + var added = document.querySelector('#piecrust-debug-notification-msg' + noteId); + added.remove(); +}; + +/////////////////////////////////////////////////////////////////////////////// + +function _get_extension(name) { + var ext = null; + var dotIdx = name.lastIndexOf('.'); + if (dotIdx > 0) + ext = name.substr(dotIdx + 1); + return ext; +} + +function _get_basename(name) { + var filename = name; + var slashIdx = name.lastIndexOf('/'); + if (slashIdx > 0) + filename = name.substr(slashIdx + 1); + return filename; +} + +var _regex_cache_bust = /\?\d+$/; + +function _is_path_match(path1, path2) { + path1 = path1.replace(_regex_cache_bust, ''); + console.log("Matching:", path1, path2) + return path1.endsWith(path2); +}; + +function _add_cache_bust(path, cache_bust) { + path = path.replace(_regex_cache_bust, ''); + return path + cache_bust; +} + +/////////////////////////////////////////////////////////////////////////////// + +AssetReloader = function() { + this._imgExts = ['jpg', 'jpeg', 'png', 'gif', 'svg']; + this._imgReloader = new ImageReloader(); + this._cssReloader = new CssReloader(); }; -eventSource.addEventListener('pipeline_success', function(e) { - var placeholder = document.getElementById('piecrust-debug-info-pipeline-status'); - //if (placeholder.firstChild !== null) - placeholder.removeChild(placeholder.firstChild); -}); - -eventSource.addEventListener('pipeline_error', function(e) { - var obj = JSON.parse(e.data); +AssetReloader.prototype.reloadAsset = function(name) { + var ext = _get_extension(name); + var filename = _get_basename(name); - var outer = document.createElement('div'); - outer.style = 'padding: 1em;'; - for (var i = 0; i < obj.assets.length; ++i) { - var item = obj.assets[i]; - var markup = ( - '<p>Error processing: <span style="font-family: monospace;">' + - item.path + '</span></p>\n' + - '<ul>'); - for (var j = 0; j < item.errors.length; ++j) { - markup += ( - '<li style="font-family: monospace;">' + - item.errors[j] + - '</li>\n'); - } - markup += '</ul>\n'; - var entry = document.createElement('div'); - entry.innerHTML = markup; - outer.appendChild(entry); + if (ext == 'css') { + return this._cssReloader.reloadStylesheet(filename); + } + if (this._imgExts.indexOf(ext) >= 0) { + return this._imgReloader.reloadImage(filename); } - var placeholder = document.getElementById('piecrust-debug-info-pipeline-status'); - placeholder.appendChild(outer); -}); + console.log("Don't know how to reload", filename); + return false; +}; + +/////////////////////////////////////////////////////////////////////////////// + +CssReloader = function() { +}; + +CssReloader.prototype.reloadStylesheet = function(name) { + var result = false; + var sheets = document.styleSheets; + var cacheBust = '?' + new Date().getTime(); + for (var i = 0; i < sheets.length; ++i) { + var sheet = sheets[i]; + if (_is_path_match(sheet.href, name)) { + sheet.ownerNode.href = _add_cache_bust(sheet.href, cacheBust); + result = true; + } + } + return result; +}; + +/////////////////////////////////////////////////////////////////////////////// + +ImageReloader = function() { + this._imgStyles = [ + { selector: 'background', styleNames: ['backgroundImage'] }, + ]; + this._regexCssUrl = /\burl\s*\(([^)]+)\)/; +}; + +ImageReloader.prototype.reloadImage = function(name) { + var result = false; + var imgs = document.images; + var cacheBust = '?' + new Date().getTime(); + for (var i = 0; i < imgs.length; ++i) { + var img = imgs[i]; + if (_is_path_match(img.src, name)) { + img.src = _add_cache_bust(img.src, cacheBust); + result = true; + } + } + for (var i = 0; i < this._imgStyles.length; ++i) { + var imgInfo = this._imgStyles[i]; + var domImgs = document.querySelectorAll( + "[style*=" + imgInfo.selector + "]"); + for (var j = 0; j < domImgs.length; ++j) { + var img = domImgs[j]; + result |= this._reloadStyleImage(img.style, imgInfo.styleNames, + name, cacheBust); + } + } + for (var i = 0; i < document.styleSheets.length; ++i) { + var styleSheet = document.styleSheets[i]; + result |= this._reloadStylesheetImage(styleSheet, name, cacheBust); + } + return result; +}; +ImageReloader.prototype._reloadStyleImage = function(style, styleNames, path, + cacheBust) { + var result = false; + for (var i = 0; i < styleNames.length; ++i) { + var value = style[styleNames[i]]; + if ((typeof value) == 'string') { + m = this._regexCssUrl.exec(value); + if (m != null) { + var m_clean = m[1].replace(/^['"]/, ''); + m_clean = m_clean.replace(/['"]$/, ''); + if (_is_path_match(m_clean, path)) { + m_clean = _add_cache_bust(m_clean, cacheBust); + style[styleNames[i]] = 'url("' + m_clean + '")'; + result = true; + } + } + } + } + return result; +}; + +ImageReloader.prototype._reloadStylesheetImage = function(styleSheet, path, + cacheBust) { + try { + var rules = styleSheet.cssRules; + } catch (e) { + // Things like remote CSS stylesheets (e.g. a Google Fonts ones) + // will triger a SecurityException here, so just ignore that. + return; + } + + var result = false; + for (var i = 0; i < rules.length; ++i) { + var rule = rules[i]; + switch (rule.type) { + case CSSRule.IMPORT_RULE: + result |= this._reloadStylesheetImage(rule.styleSheet, path, + cacheBust); + break; + case CSSRule.MEDIA_RULE: + result |= this._reloadStylesheetImage(rule, path, cacheBust); + break; + case CSSRule.STYLE_RULE: + for (var j = 0; j < this._imgStyles.length; ++j) { + var imgInfo = this._imgStyles[j]; + result |= this._reloadStyleImage( + rule.style, imgInfo.styleNames, path, cacheBust); + } + break; + } + } + return result; +}; + +/////////////////////////////////////////////////////////////////////////////// + +var notification = new NotificationArea(); +var assetReloader = new AssetReloader(); + +window.onload = function() { + var cacheBust = '?' + new Date().getTime(); + + var style = document.createElement('link'); + style.rel = 'stylesheet'; + style.type = 'text/css'; + style.href = '/__piecrust_static/piecrust-debug-info.css' + cacheBust; + document.head.appendChild(style); +}; + + +window.onbeforeunload = function(e) { + if (eventSource != null) + eventSource.close(); +}; +