├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example ├── dummy-server.js └── res │ └── livereload.js ├── lib ├── connection.coffee └── server.coffee ├── package.json └── test └── server_test.iced /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !example/*.js 3 | !example/res/*.js 4 | node_modules 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.coffee 3 | *.iced 4 | test/ 5 | example/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012, Andrey Tarantsov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveReload server in Node.js 2 | 3 | Implementation of the server side of the [LiveReload protocol](https://github.com/livereload/livereload-protocol) in Node.js; a component of LiveReload.app and some other projects. 4 | 5 | Status: beta (expected to work, but not tested in production yet). 6 | 7 | 8 | ## Installation 9 | 10 | npm install livereload-server 11 | 12 | 13 | ## Example 14 | 15 | See example/dummy-server.js for a runnable version of this code: 16 | 17 | var fs = require('fs'); 18 | var Path = require('path'); 19 | var LRWebSocketServer = require('livereload-server'); 20 | 21 | // id, name, version identifies your app; 22 | // protocols specifies the versions of subprotocols you support 23 | var server = new LRWebSocketServer({ id: "com.example.acme", name: "Acme", version: "1.0", protocols: { monitoring: 7, saving: 1 } }); 24 | 25 | server.on('connected', function(connection) { 26 | console.log("Client connected (%s)", connection.id); 27 | }); 28 | 29 | server.on('disconnected', function(connection) { 30 | console.log("Client disconnected (%s)", connection.id); 31 | }); 32 | 33 | server.on('command', function(connection, message) { 34 | console.log("Received command %s: %j", message.command, message); 35 | }); 36 | 37 | server.on('error', function(err, connection) { 38 | console.log("Error (%s): %s", connection.id, err.message); 39 | }); 40 | 41 | server.on('livereload.js', function(request, response) { 42 | console.log("Serving livereload.js."); 43 | fs.readFile(Path.join(__dirname, 'res/livereload.js'), 'utf8', function(err, data) { 44 | if (err) throw err; 45 | 46 | response.writeHead(200, {'Content-Length': data.length, 'Content-Type': 'text/javascript'}); 47 | response.end(data); 48 | }); 49 | }); 50 | 51 | server.on('httprequest', function(url, request, response) { 52 | response.writeHead(404); 53 | response.end() 54 | }); 55 | 56 | server.listen(function(err) { 57 | if (err) { 58 | console.error("Listening failed: %s", err.message); 59 | return; 60 | } 61 | console.log("Listening on port %d.", server.port); 62 | }); 63 | 64 | 65 | 66 | ## License 67 | 68 | © 2012, Andrey Tarantsov, distributed under the MIT license. 69 | -------------------------------------------------------------------------------- /example/dummy-server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var Path = require('path'); 3 | var LRWebSocketServer = require('../lib/server'); 4 | 5 | // id, name, version identifies your app; 6 | // protocols specifies the versions of subprotocols you support 7 | var server = new LRWebSocketServer({ id: "com.example.acme", name: "Acme", version: "1.0", protocols: { monitoring: 7, saving: 1 } }); 8 | 9 | server.on('connected', function(connection) { 10 | console.log("Client connected (%s)", connection.id); 11 | }); 12 | 13 | server.on('disconnected', function(connection) { 14 | console.log("Client disconnected (%s)", connection.id); 15 | }); 16 | 17 | server.on('command', function(connection, message) { 18 | console.log("Received command %s: %j", message.command, message); 19 | }); 20 | 21 | server.on('error', function(err, connection) { 22 | console.log("Error (%s): %s", connection.id, err.message); 23 | }); 24 | 25 | server.on('livereload.js', function(request, response) { 26 | console.log("Serving livereload.js."); 27 | fs.readFile(Path.join(__dirname, 'res/livereload.js'), 'utf8', function(err, data) { 28 | if (err) throw err; 29 | 30 | response.writeHead(200, {'Content-Length': data.length, 'Content-Type': 'text/javascript'}); 31 | response.end(data); 32 | }); 33 | }); 34 | 35 | server.on('httprequest', function(url, request, response) { 36 | response.writeHead(404); 37 | response.end() 38 | }); 39 | 40 | server.listen(function(err) { 41 | if (err) { 42 | console.error("Listening failed: %s", err.message); 43 | return; 44 | } 45 | console.log("Listening on port %d.", server.port); 46 | }); 47 | -------------------------------------------------------------------------------- /example/res/livereload.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var __customevents = {}, __protocol = {}, __connector = {}, __timer = {}, __options = {}, __reloader = {}, __livereload = {}, __less = {}, __startup = {}; 3 | 4 | // customevents 5 | var CustomEvents; 6 | CustomEvents = { 7 | bind: function(element, eventName, handler) { 8 | if (element.addEventListener) { 9 | return element.addEventListener(eventName, handler, false); 10 | } else if (element.attachEvent) { 11 | element[eventName] = 1; 12 | return element.attachEvent('onpropertychange', function(event) { 13 | if (event.propertyName === eventName) { 14 | return handler(); 15 | } 16 | }); 17 | } else { 18 | throw new Error("Attempt to attach custom event " + eventName + " to something which isn't a DOMElement"); 19 | } 20 | }, 21 | fire: function(element, eventName) { 22 | var event; 23 | if (element.addEventListener) { 24 | event = document.createEvent('HTMLEvents'); 25 | event.initEvent(eventName, true, true); 26 | return document.dispatchEvent(event); 27 | } else if (element.attachEvent) { 28 | if (element[eventName]) { 29 | return element[eventName]++; 30 | } 31 | } else { 32 | throw new Error("Attempt to fire custom event " + eventName + " on something which isn't a DOMElement"); 33 | } 34 | } 35 | }; 36 | __customevents.bind = CustomEvents.bind; 37 | __customevents.fire = CustomEvents.fire; 38 | 39 | // protocol 40 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError; 41 | var __indexOf = Array.prototype.indexOf || function(item) { 42 | for (var i = 0, l = this.length; i < l; i++) { 43 | if (this[i] === item) return i; 44 | } 45 | return -1; 46 | }; 47 | __protocol.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; 48 | __protocol.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; 49 | __protocol.ProtocolError = ProtocolError = (function() { 50 | function ProtocolError(reason, data) { 51 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; 52 | } 53 | return ProtocolError; 54 | })(); 55 | __protocol.Parser = Parser = (function() { 56 | function Parser(handlers) { 57 | this.handlers = handlers; 58 | this.reset(); 59 | } 60 | Parser.prototype.reset = function() { 61 | return this.protocol = null; 62 | }; 63 | Parser.prototype.process = function(data) { 64 | var command, message, options, _ref; 65 | try { 66 | if (!(this.protocol != null)) { 67 | if (data.match(/^!!ver:([\d.]+)$/)) { 68 | this.protocol = 6; 69 | } else if (message = this._parseMessage(data, ['hello'])) { 70 | if (!message.protocols.length) { 71 | throw new ProtocolError("no protocols specified in handshake message"); 72 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { 73 | this.protocol = 7; 74 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { 75 | this.protocol = 6; 76 | } else { 77 | throw new ProtocolError("no supported protocols found"); 78 | } 79 | } 80 | return this.handlers.connected(this.protocol); 81 | } else if (this.protocol === 6) { 82 | message = JSON.parse(data); 83 | if (!message.length) { 84 | throw new ProtocolError("protocol 6 messages must be arrays"); 85 | } 86 | command = message[0], options = message[1]; 87 | if (command !== 'refresh') { 88 | throw new ProtocolError("unknown protocol 6 command"); 89 | } 90 | return this.handlers.message({ 91 | command: 'reload', 92 | path: options.path, 93 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true 94 | }); 95 | } else { 96 | message = this._parseMessage(data, ['reload', 'alert']); 97 | return this.handlers.message(message); 98 | } 99 | } catch (e) { 100 | if (e instanceof ProtocolError) { 101 | return this.handlers.error(e); 102 | } else { 103 | throw e; 104 | } 105 | } 106 | }; 107 | Parser.prototype._parseMessage = function(data, validCommands) { 108 | var message, _ref; 109 | try { 110 | message = JSON.parse(data); 111 | } catch (e) { 112 | throw new ProtocolError('unparsable JSON', data); 113 | } 114 | if (!message.command) { 115 | throw new ProtocolError('missing "command" key', data); 116 | } 117 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { 118 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); 119 | } 120 | return message; 121 | }; 122 | return Parser; 123 | })(); 124 | 125 | // connector 126 | // Generated by CoffeeScript 1.3.3 127 | var Connector, PROTOCOL_6, PROTOCOL_7, Parser, Version, _ref; 128 | 129 | _ref = __protocol, Parser = _ref.Parser, PROTOCOL_6 = _ref.PROTOCOL_6, PROTOCOL_7 = _ref.PROTOCOL_7; 130 | 131 | Version = '2.0.8'; 132 | 133 | __connector.Connector = Connector = (function() { 134 | 135 | function Connector(options, WebSocket, Timer, handlers) { 136 | var _this = this; 137 | this.options = options; 138 | this.WebSocket = WebSocket; 139 | this.Timer = Timer; 140 | this.handlers = handlers; 141 | this._uri = "ws://" + this.options.host + ":" + this.options.port + "/livereload"; 142 | this._nextDelay = this.options.mindelay; 143 | this._connectionDesired = false; 144 | this.protocol = 0; 145 | this.protocolParser = new Parser({ 146 | connected: function(protocol) { 147 | _this.protocol = protocol; 148 | _this._handshakeTimeout.stop(); 149 | _this._nextDelay = _this.options.mindelay; 150 | _this._disconnectionReason = 'broken'; 151 | return _this.handlers.connected(protocol); 152 | }, 153 | error: function(e) { 154 | _this.handlers.error(e); 155 | return _this._closeOnError(); 156 | }, 157 | message: function(message) { 158 | return _this.handlers.message(message); 159 | } 160 | }); 161 | this._handshakeTimeout = new Timer(function() { 162 | if (!_this._isSocketConnected()) { 163 | return; 164 | } 165 | _this._disconnectionReason = 'handshake-timeout'; 166 | return _this.socket.close(); 167 | }); 168 | this._reconnectTimer = new Timer(function() { 169 | if (!_this._connectionDesired) { 170 | return; 171 | } 172 | return _this.connect(); 173 | }); 174 | this.connect(); 175 | } 176 | 177 | Connector.prototype._isSocketConnected = function() { 178 | return this.socket && this.socket.readyState === this.WebSocket.OPEN; 179 | }; 180 | 181 | Connector.prototype.connect = function() { 182 | var _this = this; 183 | this._connectionDesired = true; 184 | if (this._isSocketConnected()) { 185 | return; 186 | } 187 | this._reconnectTimer.stop(); 188 | this._disconnectionReason = 'cannot-connect'; 189 | this.protocolParser.reset(); 190 | this.handlers.connecting(); 191 | this.socket = new this.WebSocket(this._uri); 192 | this.socket.onopen = function(e) { 193 | return _this._onopen(e); 194 | }; 195 | this.socket.onclose = function(e) { 196 | return _this._onclose(e); 197 | }; 198 | this.socket.onmessage = function(e) { 199 | return _this._onmessage(e); 200 | }; 201 | return this.socket.onerror = function(e) { 202 | return _this._onerror(e); 203 | }; 204 | }; 205 | 206 | Connector.prototype.disconnect = function() { 207 | this._connectionDesired = false; 208 | this._reconnectTimer.stop(); 209 | if (!this._isSocketConnected()) { 210 | return; 211 | } 212 | this._disconnectionReason = 'manual'; 213 | return this.socket.close(); 214 | }; 215 | 216 | Connector.prototype._scheduleReconnection = function() { 217 | if (!this._connectionDesired) { 218 | return; 219 | } 220 | if (!this._reconnectTimer.running) { 221 | this._reconnectTimer.start(this._nextDelay); 222 | return this._nextDelay = Math.min(this.options.maxdelay, this._nextDelay * 2); 223 | } 224 | }; 225 | 226 | Connector.prototype.sendCommand = function(command) { 227 | if (this.protocol == null) { 228 | return; 229 | } 230 | return this._sendCommand(command); 231 | }; 232 | 233 | Connector.prototype._sendCommand = function(command) { 234 | return this.socket.send(JSON.stringify(command)); 235 | }; 236 | 237 | Connector.prototype._closeOnError = function() { 238 | this._handshakeTimeout.stop(); 239 | this._disconnectionReason = 'error'; 240 | return this.socket.close(); 241 | }; 242 | 243 | Connector.prototype._onopen = function(e) { 244 | var hello; 245 | this.handlers.socketConnected(); 246 | this._disconnectionReason = 'handshake-failed'; 247 | hello = { 248 | command: 'hello', 249 | protocols: [PROTOCOL_6, PROTOCOL_7] 250 | }; 251 | hello.ver = Version; 252 | if (this.options.ext) { 253 | hello.ext = this.options.ext; 254 | } 255 | if (this.options.extver) { 256 | hello.extver = this.options.extver; 257 | } 258 | if (this.options.snipver) { 259 | hello.snipver = this.options.snipver; 260 | } 261 | this._sendCommand(hello); 262 | return this._handshakeTimeout.start(this.options.handshake_timeout); 263 | }; 264 | 265 | Connector.prototype._onclose = function(e) { 266 | this.protocol = 0; 267 | this.handlers.disconnected(this._disconnectionReason, this._nextDelay); 268 | return this._scheduleReconnection(); 269 | }; 270 | 271 | Connector.prototype._onerror = function(e) {}; 272 | 273 | Connector.prototype._onmessage = function(e) { 274 | return this.protocolParser.process(e.data); 275 | }; 276 | 277 | return Connector; 278 | 279 | })(); 280 | 281 | // timer 282 | var Timer; 283 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 284 | __timer.Timer = Timer = (function() { 285 | function Timer(func) { 286 | this.func = func; 287 | this.running = false; 288 | this.id = null; 289 | this._handler = __bind(function() { 290 | this.running = false; 291 | this.id = null; 292 | return this.func(); 293 | }, this); 294 | } 295 | Timer.prototype.start = function(timeout) { 296 | if (this.running) { 297 | clearTimeout(this.id); 298 | } 299 | this.id = setTimeout(this._handler, timeout); 300 | return this.running = true; 301 | }; 302 | Timer.prototype.stop = function() { 303 | if (this.running) { 304 | clearTimeout(this.id); 305 | this.running = false; 306 | return this.id = null; 307 | } 308 | }; 309 | return Timer; 310 | })(); 311 | Timer.start = function(timeout, func) { 312 | return setTimeout(func, timeout); 313 | }; 314 | 315 | // options 316 | var Options; 317 | __options.Options = Options = (function() { 318 | function Options() { 319 | this.host = null; 320 | this.port = 35729; 321 | this.snipver = null; 322 | this.ext = null; 323 | this.extver = null; 324 | this.mindelay = 1000; 325 | this.maxdelay = 60000; 326 | this.handshake_timeout = 5000; 327 | } 328 | Options.prototype.set = function(name, value) { 329 | switch (typeof this[name]) { 330 | case 'undefined': 331 | break; 332 | case 'number': 333 | return this[name] = +value; 334 | default: 335 | return this[name] = value; 336 | } 337 | }; 338 | return Options; 339 | })(); 340 | Options.extract = function(document) { 341 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len2, _ref, _ref2; 342 | _ref = document.getElementsByTagName('script'); 343 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 344 | element = _ref[_i]; 345 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { 346 | options = new Options(); 347 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { 348 | options.host = mm[1]; 349 | if (mm[2]) { 350 | options.port = parseInt(mm[2], 10); 351 | } 352 | } 353 | if (m[2]) { 354 | _ref2 = m[2].split('&'); 355 | for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { 356 | pair = _ref2[_j]; 357 | if ((keyAndValue = pair.split('=')).length > 1) { 358 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); 359 | } 360 | } 361 | } 362 | return options; 363 | } 364 | } 365 | return null; 366 | }; 367 | 368 | // reloader 369 | // Generated by CoffeeScript 1.3.1 370 | (function() { 371 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; 372 | 373 | splitUrl = function(url) { 374 | var hash, index, params; 375 | if ((index = url.indexOf('#')) >= 0) { 376 | hash = url.slice(index); 377 | url = url.slice(0, index); 378 | } else { 379 | hash = ''; 380 | } 381 | if ((index = url.indexOf('?')) >= 0) { 382 | params = url.slice(index); 383 | url = url.slice(0, index); 384 | } else { 385 | params = ''; 386 | } 387 | return { 388 | url: url, 389 | params: params, 390 | hash: hash 391 | }; 392 | }; 393 | 394 | pathFromUrl = function(url) { 395 | var path; 396 | url = splitUrl(url).url; 397 | if (url.indexOf('file://') === 0) { 398 | path = url.replace(/^file:\/\/(localhost)?/, ''); 399 | } else { 400 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); 401 | } 402 | return decodeURIComponent(path); 403 | }; 404 | 405 | pickBestMatch = function(path, objects, pathFunc) { 406 | var bestMatch, object, score, _i, _len; 407 | bestMatch = { 408 | score: 0 409 | }; 410 | for (_i = 0, _len = objects.length; _i < _len; _i++) { 411 | object = objects[_i]; 412 | score = numberOfMatchingSegments(path, pathFunc(object)); 413 | if (score > bestMatch.score) { 414 | bestMatch = { 415 | object: object, 416 | score: score 417 | }; 418 | } 419 | } 420 | if (bestMatch.score > 0) { 421 | return bestMatch; 422 | } else { 423 | return null; 424 | } 425 | }; 426 | 427 | numberOfMatchingSegments = function(path1, path2) { 428 | var comps1, comps2, eqCount, len; 429 | path1 = path1.replace(/^\/+/, '').toLowerCase(); 430 | path2 = path2.replace(/^\/+/, '').toLowerCase(); 431 | if (path1 === path2) { 432 | return 10000; 433 | } 434 | comps1 = path1.split('/').reverse(); 435 | comps2 = path2.split('/').reverse(); 436 | len = Math.min(comps1.length, comps2.length); 437 | eqCount = 0; 438 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { 439 | ++eqCount; 440 | } 441 | return eqCount; 442 | }; 443 | 444 | pathsMatch = function(path1, path2) { 445 | return numberOfMatchingSegments(path1, path2) > 0; 446 | }; 447 | 448 | IMAGE_STYLES = [ 449 | { 450 | selector: 'background', 451 | styleNames: ['backgroundImage'] 452 | }, { 453 | selector: 'border', 454 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] 455 | } 456 | ]; 457 | 458 | __reloader.Reloader = Reloader = (function() { 459 | 460 | Reloader.name = 'Reloader'; 461 | 462 | function Reloader(window, console, Timer) { 463 | this.window = window; 464 | this.console = console; 465 | this.Timer = Timer; 466 | this.document = this.window.document; 467 | this.importCacheWaitPeriod = 200; 468 | this.plugins = []; 469 | } 470 | 471 | Reloader.prototype.addPlugin = function(plugin) { 472 | return this.plugins.push(plugin); 473 | }; 474 | 475 | Reloader.prototype.analyze = function(callback) { 476 | return results; 477 | }; 478 | 479 | Reloader.prototype.reload = function(path, options) { 480 | var plugin, _base, _i, _len, _ref; 481 | this.options = options; 482 | if ((_base = this.options).stylesheetReloadTimeout == null) { 483 | _base.stylesheetReloadTimeout = 15000; 484 | } 485 | _ref = this.plugins; 486 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 487 | plugin = _ref[_i]; 488 | if (plugin.reload && plugin.reload(path, options)) { 489 | return; 490 | } 491 | } 492 | if (options.liveCSS) { 493 | if (path.match(/\.css$/i)) { 494 | if (this.reloadStylesheet(path)) { 495 | return; 496 | } 497 | } 498 | } 499 | if (options.liveImg) { 500 | if (path.match(/\.(jpe?g|png|gif)$/i)) { 501 | this.reloadImages(path); 502 | return; 503 | } 504 | } 505 | return this.reloadPage(); 506 | }; 507 | 508 | Reloader.prototype.reloadPage = function() { 509 | return this.window.document.location.reload(); 510 | }; 511 | 512 | Reloader.prototype.reloadImages = function(path) { 513 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; 514 | expando = this.generateUniqueString(); 515 | _ref = this.document.images; 516 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 517 | img = _ref[_i]; 518 | if (pathsMatch(path, pathFromUrl(img.src))) { 519 | img.src = this.generateCacheBustUrl(img.src, expando); 520 | } 521 | } 522 | if (this.document.querySelectorAll) { 523 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 524 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; 525 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); 526 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 527 | img = _ref2[_k]; 528 | this.reloadStyleImages(img.style, styleNames, path, expando); 529 | } 530 | } 531 | } 532 | if (this.document.styleSheets) { 533 | _ref3 = this.document.styleSheets; 534 | _results = []; 535 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 536 | styleSheet = _ref3[_l]; 537 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); 538 | } 539 | return _results; 540 | } 541 | }; 542 | 543 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { 544 | var rule, rules, styleNames, _i, _j, _len, _len1; 545 | try { 546 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 547 | } catch (e) { 548 | 549 | } 550 | if (!rules) { 551 | return; 552 | } 553 | for (_i = 0, _len = rules.length; _i < _len; _i++) { 554 | rule = rules[_i]; 555 | switch (rule.type) { 556 | case CSSRule.IMPORT_RULE: 557 | this.reloadStylesheetImages(rule.styleSheet, path, expando); 558 | break; 559 | case CSSRule.STYLE_RULE: 560 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 561 | styleNames = IMAGE_STYLES[_j].styleNames; 562 | this.reloadStyleImages(rule.style, styleNames, path, expando); 563 | } 564 | break; 565 | case CSSRule.MEDIA_RULE: 566 | this.reloadStylesheetImages(rule, path, expando); 567 | } 568 | } 569 | }; 570 | 571 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { 572 | var newValue, styleName, value, _i, _len, 573 | _this = this; 574 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) { 575 | styleName = styleNames[_i]; 576 | value = style[styleName]; 577 | if (typeof value === 'string') { 578 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, function(match, src) { 579 | if (pathsMatch(path, pathFromUrl(src))) { 580 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; 581 | } else { 582 | return match; 583 | } 584 | }); 585 | if (newValue !== value) { 586 | style[styleName] = newValue; 587 | } 588 | } 589 | } 590 | }; 591 | 592 | Reloader.prototype.reloadStylesheet = function(path) { 593 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, 594 | _this = this; 595 | links = (function() { 596 | var _i, _len, _ref, _results; 597 | _ref = this.document.getElementsByTagName('link'); 598 | _results = []; 599 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 600 | link = _ref[_i]; 601 | if (link.rel === 'stylesheet' && !link.__LiveReload_pendingRemoval) { 602 | _results.push(link); 603 | } 604 | } 605 | return _results; 606 | }).call(this); 607 | imported = []; 608 | _ref = this.document.getElementsByTagName('style'); 609 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 610 | style = _ref[_i]; 611 | if (style.sheet) { 612 | this.collectImportedStylesheets(style, style.sheet, imported); 613 | } 614 | } 615 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) { 616 | link = links[_j]; 617 | this.collectImportedStylesheets(link, link.sheet, imported); 618 | } 619 | if (this.window.StyleFix && this.document.querySelectorAll) { 620 | _ref1 = this.document.querySelectorAll('style[data-href]'); 621 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { 622 | style = _ref1[_k]; 623 | links.push(style); 624 | } 625 | } 626 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); 627 | match = pickBestMatch(path, links.concat(imported), function(l) { 628 | return pathFromUrl(_this.linkHref(l)); 629 | }); 630 | if (match) { 631 | if (match.object.rule) { 632 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); 633 | this.reattachImportedRule(match.object); 634 | } else { 635 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); 636 | this.reattachStylesheetLink(match.object); 637 | } 638 | } else { 639 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one"); 640 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) { 641 | link = links[_l]; 642 | this.reattachStylesheetLink(link); 643 | } 644 | } 645 | return true; 646 | }; 647 | 648 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { 649 | var index, rule, rules, _i, _len; 650 | try { 651 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 652 | } catch (e) { 653 | 654 | } 655 | if (rules && rules.length) { 656 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { 657 | rule = rules[index]; 658 | switch (rule.type) { 659 | case CSSRule.CHARSET_RULE: 660 | continue; 661 | case CSSRule.IMPORT_RULE: 662 | result.push({ 663 | link: link, 664 | rule: rule, 665 | index: index, 666 | href: rule.href 667 | }); 668 | this.collectImportedStylesheets(link, rule.styleSheet, result); 669 | break; 670 | default: 671 | break; 672 | } 673 | } 674 | } 675 | }; 676 | 677 | Reloader.prototype.waitUntilCssLoads = function(clone, func) { 678 | var callbackExecuted, executeCallback, poll, 679 | _this = this; 680 | callbackExecuted = false; 681 | executeCallback = function() { 682 | if (callbackExecuted) { 683 | return; 684 | } 685 | callbackExecuted = true; 686 | return func(); 687 | }; 688 | clone.onload = function() { 689 | console.log("onload!"); 690 | _this.knownToSupportCssOnLoad = true; 691 | return executeCallback(); 692 | }; 693 | if (!this.knownToSupportCssOnLoad) { 694 | (poll = function() { 695 | if (clone.sheet) { 696 | console.log("polling!"); 697 | return executeCallback(); 698 | } else { 699 | return _this.Timer.start(50, poll); 700 | } 701 | })(); 702 | } 703 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); 704 | }; 705 | 706 | Reloader.prototype.linkHref = function(link) { 707 | return link.href || link.getAttribute('data-href'); 708 | }; 709 | 710 | Reloader.prototype.reattachStylesheetLink = function(link) { 711 | var clone, parent, 712 | _this = this; 713 | if (link.__LiveReload_pendingRemoval) { 714 | return; 715 | } 716 | link.__LiveReload_pendingRemoval = true; 717 | if (link.tagName === 'STYLE') { 718 | clone = this.document.createElement('link'); 719 | clone.rel = 'stylesheet'; 720 | clone.media = link.media; 721 | clone.disabled = link.disabled; 722 | } else { 723 | clone = link.cloneNode(false); 724 | } 725 | clone.href = this.generateCacheBustUrl(this.linkHref(link)); 726 | parent = link.parentNode; 727 | if (parent.lastChild === link) { 728 | parent.appendChild(clone); 729 | } else { 730 | parent.insertBefore(clone, link.nextSibling); 731 | } 732 | return this.waitUntilCssLoads(clone, function() { 733 | var additionalWaitingTime; 734 | if (/AppleWebKit/.test(navigator.userAgent)) { 735 | additionalWaitingTime = 5; 736 | } else { 737 | additionalWaitingTime = 200; 738 | } 739 | return _this.Timer.start(additionalWaitingTime, function() { 740 | var _ref; 741 | if (!link.parentNode) { 742 | return; 743 | } 744 | link.parentNode.removeChild(link); 745 | clone.onreadystatechange = null; 746 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; 747 | }); 748 | }); 749 | }; 750 | 751 | Reloader.prototype.reattachImportedRule = function(_arg) { 752 | var href, index, link, media, newRule, parent, rule, tempLink, 753 | _this = this; 754 | rule = _arg.rule, index = _arg.index, link = _arg.link; 755 | parent = rule.parentStyleSheet; 756 | href = this.generateCacheBustUrl(rule.href); 757 | media = rule.media.length ? [].join.call(rule.media, ', ') : ''; 758 | newRule = "@import url(\"" + href + "\") " + media + ";"; 759 | rule.__LiveReload_newHref = href; 760 | tempLink = this.document.createElement("link"); 761 | tempLink.rel = 'stylesheet'; 762 | tempLink.href = href; 763 | tempLink.__LiveReload_pendingRemoval = true; 764 | if (link.parentNode) { 765 | link.parentNode.insertBefore(tempLink, link); 766 | } 767 | return this.Timer.start(this.importCacheWaitPeriod, function() { 768 | if (tempLink.parentNode) { 769 | tempLink.parentNode.removeChild(tempLink); 770 | } 771 | if (rule.__LiveReload_newHref !== href) { 772 | return; 773 | } 774 | parent.insertRule(newRule, index); 775 | parent.deleteRule(index + 1); 776 | rule = parent.cssRules[index]; 777 | rule.__LiveReload_newHref = href; 778 | return _this.Timer.start(_this.importCacheWaitPeriod, function() { 779 | if (rule.__LiveReload_newHref !== href) { 780 | return; 781 | } 782 | parent.insertRule(newRule, index); 783 | return parent.deleteRule(index + 1); 784 | }); 785 | }); 786 | }; 787 | 788 | Reloader.prototype.generateUniqueString = function() { 789 | return 'livereload=' + Date.now(); 790 | }; 791 | 792 | Reloader.prototype.generateCacheBustUrl = function(url, expando) { 793 | var hash, oldParams, params, _ref; 794 | if (expando == null) { 795 | expando = this.generateUniqueString(); 796 | } 797 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; 798 | if (this.options.overrideURL) { 799 | if (url.indexOf(this.options.serverURL) < 0) { 800 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); 801 | } 802 | } 803 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { 804 | return "" + sep + expando; 805 | }); 806 | if (params === oldParams) { 807 | if (oldParams.length === 0) { 808 | params = "?" + expando; 809 | } else { 810 | params = "" + oldParams + "&" + expando; 811 | } 812 | } 813 | return url + params + hash; 814 | }; 815 | 816 | return Reloader; 817 | 818 | })(); 819 | 820 | }).call(this); 821 | 822 | // livereload 823 | var Connector, LiveReload, Options, Reloader, Timer; 824 | 825 | Connector = __connector.Connector; 826 | 827 | Timer = __timer.Timer; 828 | 829 | Options = __options.Options; 830 | 831 | Reloader = __reloader.Reloader; 832 | 833 | __livereload.LiveReload = LiveReload = (function() { 834 | 835 | function LiveReload(window) { 836 | var _this = this; 837 | this.window = window; 838 | this.listeners = {}; 839 | this.plugins = []; 840 | this.pluginIdentifiers = {}; 841 | this.console = this.window.location.href.match(/LR-verbose/) && this.window.console && this.window.console.log && this.window.console.error ? this.window.console : { 842 | log: function() {}, 843 | error: function() {} 844 | }; 845 | if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) { 846 | console.error("LiveReload disabled because the browser does not seem to support web sockets"); 847 | return; 848 | } 849 | if (!(this.options = Options.extract(this.window.document))) { 850 | console.error("LiveReload disabled because it could not find its own