├── .npmignore ├── .gitignore ├── package.json ├── LICENSE ├── example ├── dummy-server.js └── res │ └── livereload.js ├── lib ├── connection.coffee └── server.coffee ├── test └── server_test.iced └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.coffee 3 | *.iced 4 | test/ 5 | example/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !example/*.js 3 | !example/res/*.js 4 | node_modules 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Andrey Tarantsov ", 3 | "name": "livereload-server", 4 | "description": "LiveReload 3 web socket and http server", 5 | "version": "0.2.3", 6 | "homepage": "https://github.com/livereload/livereload-server", 7 | "repository": { 8 | "url": "git://github.com/livereload/livereload-server" 9 | }, 10 | "main": "lib/server.js", 11 | "scripts": { 12 | "test": "./node_modules/mocha/bin/mocha -R ${REPORTER-spec}" 13 | }, 14 | "dependencies": { 15 | "debug": "~0.7.0", 16 | "websocket.io": "~0.2.1", 17 | "livereload-protocol": "~0.2.2" 18 | }, 19 | "devDependencies": { 20 | "mocha": "~1.0.3", 21 | "ws": "~0.4.18", 22 | "iced-coffee-script": "~1.3" 23 | }, 24 | "optionalDependencies": {}, 25 | "engines": { 26 | "node": ">= 0.6.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/connection.coffee: -------------------------------------------------------------------------------- 1 | debug = require('debug')('livereload:server') 2 | 3 | { EventEmitter } = require 'events' 4 | 5 | Parser = require 'livereload-protocol' 6 | 7 | HandshakeTimeout = 1000 8 | 9 | 10 | class LRWebSocketConnection extends EventEmitter 11 | constructor: (@socket, @id, @options) -> 12 | protocols = 13 | monitoring: [Parser.protocols.MONITORING_7] 14 | conncheck: [Parser.protocols.CONN_CHECK_1] 15 | 16 | if @options.protocols.saving >= 1 17 | protocols.saving = [Parser.protocols.SAVING_1] 18 | 19 | @parser = new Parser 'server', protocols 20 | 21 | @socket.on 'message', (data) => 22 | debug "LRWebSocketConnection(#{@id}) received #{data}" 23 | @parser.received(data) 24 | 25 | @socket.on 'close', => 26 | (clearTimeout @_handshakeTimeout; @_handshakeTimeout = null) if @_handshakeTimeout 27 | @emit 'disconnected' 28 | 29 | @parser.on 'error', (err) => 30 | @socket.close() 31 | @emit 'error', err 32 | 33 | @parser.on 'command', (command) => 34 | if command.command is 'ping' 35 | @send { command: 'pong', token: command.token } 36 | else 37 | @emit 'command', command 38 | 39 | @parser.on 'connected', => 40 | (clearTimeout @_handshakeTimeout; @_handshakeTimeout = null) if @_handshakeTimeout 41 | @send @parser.hello(@options) 42 | @emit 'connected' 43 | 44 | @_handshakeTimeout = setTimeout((=> @_handshakeTimeout = null; @socket.close()), HandshakeTimeout) 45 | 46 | close: -> 47 | @socket.close() 48 | 49 | send: (command) -> 50 | @parser.sending command 51 | @socket.send JSON.stringify(command) 52 | 53 | isMonitoring: -> 54 | @parser.negotiatedProtocols?.monitoring >= 7 55 | 56 | 57 | module.exports = LRWebSocketConnection 58 | 59 | -------------------------------------------------------------------------------- /test/server_test.iced: -------------------------------------------------------------------------------- 1 | assert = require 'assert' 2 | WebSocket = require 'ws' 3 | net = require 'net' 4 | 5 | LRWebSocketServer = require '../lib/server' 6 | Parser = require 'livereload-protocol' 7 | 8 | PORT = parseInt(process.env['LRPortOverride'], 10) || 35729 9 | 10 | 11 | describe "LRWebSocketServer", -> 12 | 13 | it "should accept web socket connections", (done) -> 14 | server = new LRWebSocketServer(port: PORT, id: "com.livereload.livereload-server.test", name: "TestServer", version: "1.0") 15 | await server.listen defer (err) 16 | throw err if err 17 | 18 | ws = new WebSocket("ws://127.0.0.1:#{PORT}") 19 | 20 | await 21 | do(cb=defer()) -> 22 | await ws.on 'open', defer() 23 | ws.send JSON.stringify { command: 'hello', protocols: [Parser.protocols.MONITORING_7.url, Parser.protocols.CONN_CHECK_1.url] } 24 | cb() 25 | ws.once 'message', defer (msg) 26 | 27 | msg = JSON.parse(msg) 28 | assert.equal msg.command, 'hello' 29 | 30 | await 31 | ws.once 'message', defer(msg) 32 | ws.send JSON.stringify { command: 'ping', token: 'xyz' } 33 | 34 | msg = JSON.parse(msg) 35 | assert.equal msg.command, 'pong' 36 | assert.equal msg.token, 'xyz' 37 | 38 | server.close() 39 | done() 40 | 41 | # tests that existed in the previous version (not sure they make sense) 42 | it "should handle EADDRINUSE", (done) -> 43 | badguy = net.createServer() 44 | await badguy.listen PORT, defer(err) 45 | assert.ifError err 46 | 47 | server = new LRWebSocketServer(port: PORT, id: "com.livereload.livereload-server.test", name: "TestServer", version: "1.0") 48 | await server.listen defer (err) 49 | 50 | badguy.close() 51 | 52 | assert.ok !!err, "No error returned" 53 | assert.ok err.code == 'EADDRINUSE' 54 | 55 | server.close() if !err 56 | done() 57 | 58 | 59 | it "should return all monitoring connections" 60 | it "should retransmit incoming commands as wscommand events" 61 | it "should handle socket disconnection" 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/server.coffee: -------------------------------------------------------------------------------- 1 | debug = require('debug')('livereload:server') 2 | 3 | wsio = require 'websocket.io' 4 | http = require 'http' 5 | Url = require 'url' 6 | fs = require 'fs' 7 | 8 | { EventEmitter } = require 'events' 9 | 10 | LRWebSocketConnection = require './connection' 11 | 12 | DefaultWebSocketPort = parseInt(process.env['LRPortOverride'], 10) || 35729 13 | 14 | 15 | class LRWebSocketServer extends EventEmitter 16 | 17 | constructor: (@options) -> 18 | throw new Error("ERR_INVALID_ARG: id is required") unless @options.id 19 | throw new Error("ERR_INVALID_ARG: name is required") unless @options.name 20 | throw new Error("ERR_INVALID_ARG: version is required") unless @options.version 21 | 22 | @port = @options.port || DefaultWebSocketPort 23 | @connections = {} 24 | @activeConnections = 0 25 | @nextConnectionId = 1 26 | 27 | @options.protocols ?= {} 28 | 29 | listen: (callback) -> 30 | callbackCalled = no 31 | 32 | @httpServer = http.createServer() 33 | try 34 | @httpServer.on 'error', (err) -> 35 | if not callbackCalled 36 | callbackCalled = yes 37 | return callback(err) 38 | else 39 | throw err 40 | 41 | @httpServer.listen @port, (err) => 42 | if err 43 | callbackCalled = yes 44 | return callback(err) 45 | 46 | @httpServer.on 'request', (request, response) => 47 | request.on 'data', => 48 | # ignore; adding 'data' handler puts the stream into legacy mode on Node 0.10+ 49 | request.on 'end', => 50 | url = Url.parse(request.url, yes) 51 | if url.pathname is '/livereload.js' or url.pathname is '/xlivereload.js' 52 | @emit 'livereload.js', request, response 53 | else 54 | @emit 'httprequest', url, request, response 55 | 56 | @wsserver = wsio.attach(@httpServer) 57 | 58 | @wsserver.on 'connection', (socket) => @_createConnection(socket) 59 | 60 | callback(null) 61 | catch e 62 | callback(e) 63 | 64 | close: -> 65 | @httpServer.close() 66 | for own _, connection of @connections 67 | connection.close() 68 | return 69 | 70 | monitoringConnections: -> connection for own dummy, connection of @connections when connection.isMonitoring() 71 | 72 | monitoringConnectionCount: -> @monitoringConnections().length 73 | 74 | _createConnection: (socket) -> 75 | connection = new LRWebSocketConnection(socket, "C" + (@nextConnectionId++), @options) 76 | 77 | connection.on 'connected', => 78 | @connections[connection.id] = connection 79 | @emit 'connected', connection 80 | 81 | connection.on 'disconnected', => 82 | delete @connections[connection.id] 83 | @emit 'disconnected', connection 84 | 85 | connection.on 'command', (command) => 86 | @emit 'command', connection, command 87 | 88 | connection.on 'error', (err) => 89 | @emit 'error', err, connection 90 | 91 | return connection 92 | 93 | 94 | module.exports = LRWebSocketServer 95 | 96 | -------------------------------------------------------------------------------- /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