Mercurial > piecrust2
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 |