comparison 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
comparison
equal deleted inserted replaced
551:f2b875ecc940 552:9612cfc6455a
1 ///////////////////////////////////////////////////////////////////////////////
2 // PieCrust debug info and features
3 //
4 // This stuff is injected by PieCrust's preview server and shouldn't show up
5 // in production. It should all be self-contained in this one file.
6 ///////////////////////////////////////////////////////////////////////////////
7
1 var eventSource = new EventSource("/__piecrust_debug/pipeline_status"); 8 var eventSource = new EventSource("/__piecrust_debug/pipeline_status");
2 9
3 //window.onbeforeunload = function(e) { 10 if (eventSource != null) {
4 // console.log("Disconnecting SSE.", e); 11 eventSource.onerror = function(e) {
5 // eventSource.close(); 12 console.log("Error with SSE, closing.", e);
6 //}; 13 eventSource.close();
7 14 };
8 eventSource.onerror = function(e) { 15
9 console.log("Error with SSE, closing.", e); 16 eventSource.addEventListener('ping', function(e) {
10 eventSource.close(); 17 });
11 }; 18
12 19 eventSource.addEventListener('pipeline_success', function(e) {
13 eventSource.addEventListener('pipeline_success', function(e) { 20 var obj = JSON.parse(e.data);
14 var placeholder = document.getElementById('piecrust-debug-info-pipeline-status'); 21 console.log("Got pipeline success", obj);
15 //if (placeholder.firstChild !== null) 22
16 placeholder.removeChild(placeholder.firstChild); 23 // Check which assets were processed, and whether they are referenced
17 }); 24 // by the current page for the usual use-cases.
18 25 for (var i = 0; i < obj.assets.length; ++i) {
19 eventSource.addEventListener('pipeline_error', function(e) { 26 a = obj.assets[i];
20 var obj = JSON.parse(e.data); 27 if (assetReloader.reloadAsset(a)) {
21 28 notification.flashSuccess("Reloaded " + a);
22 var outer = document.createElement('div'); 29 }
23 outer.style = 'padding: 1em;'; 30 }
24 for (var i = 0; i < obj.assets.length; ++i) { 31 });
25 var item = obj.assets[i]; 32
26 var markup = ( 33 eventSource.addEventListener('pipeline_error', function(e) {
27 '<p>Error processing: <span style="font-family: monospace;">' + 34 var obj = JSON.parse(e.data);
28 item.path + '</span></p>\n' + 35 console.log("Got pipeline error", obj);
29 '<ul>'); 36
30 for (var j = 0; j < item.errors.length; ++j) { 37 var outer = document.createElement('div');
31 markup += ( 38 outer.style = 'padding: 1em;';
32 '<li style="font-family: monospace;">' + 39 for (var i = 0; i < obj.assets.length; ++i) {
33 item.errors[j] + 40 var item = obj.assets[i];
34 '</li>\n'); 41 var markup = (
35 } 42 '<p>Error processing: <span style="font-family: monospace;">' +
36 markup += '</ul>\n'; 43 item.path + '</span></p>\n' +
37 var entry = document.createElement('div'); 44 '<ul>');
38 entry.innerHTML = markup; 45 for (var j = 0; j < item.errors.length; ++j) {
39 outer.appendChild(entry); 46 markup += (
40 } 47 '<li style="font-family: monospace;">' +
41 48 item.errors[j] +
42 var placeholder = document.getElementById('piecrust-debug-info-pipeline-status'); 49 '</li>\n');
43 placeholder.appendChild(outer); 50 }
44 }); 51 markup += '</ul>\n';
45 52 var entry = document.createElement('div');
53 entry.innerHTML = markup;
54 outer.appendChild(entry);
55 }
56
57 var placeholder = document.getElementById('piecrust-debug-info-pipeline-status');
58 placeholder.appendChild(outer);
59 });
60 }
61
62 ///////////////////////////////////////////////////////////////////////////////
63
64 NotificationArea = function() {
65 var area = document.createElement('div');
66 area.id = 'piecrust-debug-notifications';
67 area.className = 'piecrust-debug-notifications';
68 document.querySelector('body').appendChild(area);
69
70 this._area = area;
71 this._lastId = 0;
72 };
73
74 NotificationArea.prototype.flashSuccess = function(msg) {
75 this.flashMessage(msg, 'success');
76 };
77
78 NotificationArea.prototype.flashError = function(msg) {
79 this.flashMessage(msg, 'error');
80 };
81
82 NotificationArea.prototype.flashMessage = function(msg, css_class) {
83 this._lastId += 1;
84 var thisId = this._lastId;
85 this._area.insertAdjacentHTML(
86 'afterbegin',
87 '<div id="piecrust-debug-notification-msg' + thisId + '" ' +
88 'class="piecrust-debug-notification ' +
89 'piecrust-debug-notification-' + css_class + '">' +
90 msg + '</div>');
91
92 window.setTimeout(this._discardNotification, 2000, thisId);
93 };
94
95 NotificationArea.prototype._discardNotification = function(noteId) {
96 var added = document.querySelector('#piecrust-debug-notification-msg' + noteId);
97 added.remove();
98 };
99
100 ///////////////////////////////////////////////////////////////////////////////
101
102 function _get_extension(name) {
103 var ext = null;
104 var dotIdx = name.lastIndexOf('.');
105 if (dotIdx > 0)
106 ext = name.substr(dotIdx + 1);
107 return ext;
108 }
109
110 function _get_basename(name) {
111 var filename = name;
112 var slashIdx = name.lastIndexOf('/');
113 if (slashIdx > 0)
114 filename = name.substr(slashIdx + 1);
115 return filename;
116 }
117
118 var _regex_cache_bust = /\?\d+$/;
119
120 function _is_path_match(path1, path2) {
121 path1 = path1.replace(_regex_cache_bust, '');
122 console.log("Matching:", path1, path2)
123 return path1.endsWith(path2);
124 };
125
126 function _add_cache_bust(path, cache_bust) {
127 path = path.replace(_regex_cache_bust, '');
128 return path + cache_bust;
129 }
130
131 ///////////////////////////////////////////////////////////////////////////////
132
133 AssetReloader = function() {
134 this._imgExts = ['jpg', 'jpeg', 'png', 'gif', 'svg'];
135 this._imgReloader = new ImageReloader();
136 this._cssReloader = new CssReloader();
137 };
138
139 AssetReloader.prototype.reloadAsset = function(name) {
140 var ext = _get_extension(name);
141 var filename = _get_basename(name);
142
143 if (ext == 'css') {
144 return this._cssReloader.reloadStylesheet(filename);
145 }
146 if (this._imgExts.indexOf(ext) >= 0) {
147 return this._imgReloader.reloadImage(filename);
148 }
149
150 console.log("Don't know how to reload", filename);
151 return false;
152 };
153
154 ///////////////////////////////////////////////////////////////////////////////
155
156 CssReloader = function() {
157 };
158
159 CssReloader.prototype.reloadStylesheet = function(name) {
160 var result = false;
161 var sheets = document.styleSheets;
162 var cacheBust = '?' + new Date().getTime();
163 for (var i = 0; i < sheets.length; ++i) {
164 var sheet = sheets[i];
165 if (_is_path_match(sheet.href, name)) {
166 sheet.ownerNode.href = _add_cache_bust(sheet.href, cacheBust);
167 result = true;
168 }
169 }
170 return result;
171 };
172
173 ///////////////////////////////////////////////////////////////////////////////
174
175 ImageReloader = function() {
176 this._imgStyles = [
177 { selector: 'background', styleNames: ['backgroundImage'] },
178 ];
179 this._regexCssUrl = /\burl\s*\(([^)]+)\)/;
180 };
181
182 ImageReloader.prototype.reloadImage = function(name) {
183 var result = false;
184 var imgs = document.images;
185 var cacheBust = '?' + new Date().getTime();
186 for (var i = 0; i < imgs.length; ++i) {
187 var img = imgs[i];
188 if (_is_path_match(img.src, name)) {
189 img.src = _add_cache_bust(img.src, cacheBust);
190 result = true;
191 }
192 }
193 for (var i = 0; i < this._imgStyles.length; ++i) {
194 var imgInfo = this._imgStyles[i];
195 var domImgs = document.querySelectorAll(
196 "[style*=" + imgInfo.selector + "]");
197 for (var j = 0; j < domImgs.length; ++j) {
198 var img = domImgs[j];
199 result |= this._reloadStyleImage(img.style, imgInfo.styleNames,
200 name, cacheBust);
201 }
202 }
203 for (var i = 0; i < document.styleSheets.length; ++i) {
204 var styleSheet = document.styleSheets[i];
205 result |= this._reloadStylesheetImage(styleSheet, name, cacheBust);
206 }
207 return result;
208 };
209
210 ImageReloader.prototype._reloadStyleImage = function(style, styleNames, path,
211 cacheBust) {
212 var result = false;
213 for (var i = 0; i < styleNames.length; ++i) {
214 var value = style[styleNames[i]];
215 if ((typeof value) == 'string') {
216 m = this._regexCssUrl.exec(value);
217 if (m != null) {
218 var m_clean = m[1].replace(/^['"]/, '');
219 m_clean = m_clean.replace(/['"]$/, '');
220 if (_is_path_match(m_clean, path)) {
221 m_clean = _add_cache_bust(m_clean, cacheBust);
222 style[styleNames[i]] = 'url("' + m_clean + '")';
223 result = true;
224 }
225 }
226 }
227 }
228 return result;
229 };
230
231 ImageReloader.prototype._reloadStylesheetImage = function(styleSheet, path,
232 cacheBust) {
233 try {
234 var rules = styleSheet.cssRules;
235 } catch (e) {
236 // Things like remote CSS stylesheets (e.g. a Google Fonts ones)
237 // will triger a SecurityException here, so just ignore that.
238 return;
239 }
240
241 var result = false;
242 for (var i = 0; i < rules.length; ++i) {
243 var rule = rules[i];
244 switch (rule.type) {
245 case CSSRule.IMPORT_RULE:
246 result |= this._reloadStylesheetImage(rule.styleSheet, path,
247 cacheBust);
248 break;
249 case CSSRule.MEDIA_RULE:
250 result |= this._reloadStylesheetImage(rule, path, cacheBust);
251 break;
252 case CSSRule.STYLE_RULE:
253 for (var j = 0; j < this._imgStyles.length; ++j) {
254 var imgInfo = this._imgStyles[j];
255 result |= this._reloadStyleImage(
256 rule.style, imgInfo.styleNames, path, cacheBust);
257 }
258 break;
259 }
260 }
261 return result;
262 };
263
264 ///////////////////////////////////////////////////////////////////////////////
265
266 var notification = new NotificationArea();
267 var assetReloader = new AssetReloader();
268
269 window.onload = function() {
270 var cacheBust = '?' + new Date().getTime();
271
272 var style = document.createElement('link');
273 style.rel = 'stylesheet';
274 style.type = 'text/css';
275 style.href = '/__piecrust_static/piecrust-debug-info.css' + cacheBust;
276 document.head.appendChild(style);
277 };
278
279
280 window.onbeforeunload = function(e) {
281 if (eventSource != null)
282 eventSource.close();
283 };
284