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();
+};
+