├── .gitignore ├── .travis.yml ├── Appraisals ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── config.ru ├── features ├── skip_certain_browsers.feature ├── step_definitions │ ├── given │ │ └── i_have_a_rack_app_with_live_reload.rb │ ├── then │ │ └── i_should_not_have_livereload_code.rb │ └── when │ │ └── i_make_a_request_with_headers.rb └── support │ └── env.rb ├── gemfiles ├── rails32.gemfile └── rails40.gemfile ├── index.html ├── js ├── WebSocketMain.swf ├── livereload.js ├── swfobject.js └── web_socket.js ├── lib ├── rack-livereload.rb └── rack │ ├── livereload.rb │ └── livereload │ ├── body_processor.rb │ └── processing_skip_analyzer.rb ├── rack-livereload.gemspec ├── skel └── livereload.html.erb └── spec ├── rack ├── livereload │ ├── body_processor_spec.rb │ └── processing_skip_analyzer_spec.rb └── livereload_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | gemfiles/*.lock 5 | pkg/* 6 | *.orig 7 | tmp/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | branches: 5 | only: 6 | - master 7 | gemfile: 8 | - gemfiles/rails32.gemfile 9 | - gemfiles/rails40.gemfile 10 | 11 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rails32' do 2 | gem 'rails', '~> 3.2.0' 3 | end 4 | 5 | appraise 'rails40' do 6 | gem 'rails', '~> 4.0.0' 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in rack-livereload.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :cli => '-c' do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | end 9 | 10 | guard 'livereload' do 11 | watch('index.html') 12 | end 13 | 14 | guard 'cucumber' do 15 | watch(%r{^features/.+\.feature$}) 16 | watch(%r{^features/support/.+$}) { 'features' } 17 | end 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2012 John Bintz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rack::LiveReload 2 | 3 | _This fork is deprecated: Go check out https://github.com/onesupercoder/rack-livereload instead._ 4 | 5 | 6 | [![Code Climate](https://codeclimate.com/github/johnbintz/rack-livereload.png)](https://codeclimate.com/github/johnbintz/rack-livereload) 7 | 8 | Hey, you've got [LiveReload](http://livereload.com/) in my [Rack](http://rack.rubyforge.org/)! 9 | No need for browser extensions anymore! Just plug it in your middleware stack and go! 10 | Even supports browsers without WebSockets! 11 | 12 | Use this with [guard-livereload](http://github.com/guard/guard-livereload) for maximum fun! 13 | 14 | ## Installation 15 | 16 | ### Rails 17 | 18 | Add the gem to your Gemfile. 19 | 20 | ```ruby 21 | gem "rack-livereload", group: :development 22 | ``` 23 | 24 | Then add the middleware to your Rails middleware stack by editing your `config/environments/development.rb`. 25 | 26 | ```ruby 27 | # config/environments/development.rb 28 | 29 | MyApp::Application.configure do 30 | # Add Rack::LiveReload to the bottom of the middleware stack with the default options: 31 | config.middleware.insert_after ActionDispatch::Static, Rack::LiveReload 32 | 33 | # or, if you're using better_errors: 34 | config.middleware.insert_before Rack::Lock, Rack::LiveReload 35 | 36 | # ... 37 | end 38 | ``` 39 | 40 | #### Tweaking the options 41 | 42 | ```ruby 43 | # Specifying Rack::LiveReload options. 44 | config.middleware.use(Rack::LiveReload, 45 | min_delay : 500, # default 1000 46 | max_delay : 10_000, # default 60_000 47 | live_reload_port : 56789, # default 35729 48 | host : 'myhost.cool.wow', 49 | ignore : [ %r{dont/modify\.html$} ] 50 | ) 51 | ``` 52 | 53 | In addition, Rack::LiveReload's position within middleware stack can be 54 | specified by inserting it relative to an exsiting middleware via 55 | `insert_before` or `insert_after`. See the [Rails on Rack: Adding a 56 | Middleware](http://guides.rubyonrails.org/rails_on_rack.html#adding-a-middleware) 57 | section for more detail. 58 | 59 | ### Sinatra / config.ru 60 | 61 | ``` ruby 62 | require 'rack-livereload' 63 | 64 | use Rack::LiveReload 65 | # ...or... 66 | use Rack::LiveReload, min_delay: 500, ... 67 | ``` 68 | 69 | ## How it works 70 | 71 | The necessary `script` tag to bring in a copy of [livereload.js](https://github.com/livereload/livereload-js) is 72 | injected right after the opening `head` tag in any `text/html` pages that come through. The `script` tag is built in 73 | such a way that the `HTTP_HOST` is used as the LiveReload host, so you can connect from external machines (say, to 74 | `mycomputer:3000` instead of `localhost:3000`) and as long as the LiveReload port is accessible from the external machine, 75 | you'll connect and be LiveReloading away! 76 | 77 | ### Which LiveReload script does it use? 78 | 79 | * If you've got a LiveReload watcher running on the same machine as the app that responds 80 | to `http://localhost:35729/livereload.js`, that gets used, with the hostname being changed when 81 | injected into the HTML page. 82 | * If you don't, the copy vendored with rack-livereload is used. 83 | * You can force the use of either one (and save on the cost of checking to see if that file 84 | is available) with the middleware option `:source => :vendored` or `:source => :livereload`. 85 | 86 | ### How about non-WebSocket-enabled browsers? 87 | 88 | For browsers that don't support WebSockets, but do support Flash, [web-socket-js](https://github.com/gimite/web-socket-js) 89 | is loaded. By default, this is done transparently, so you'll get a copy of swfobject.js and web_socket.js loaded even if 90 | your browser doesn't need it. The SWF WebSocket implementor won't be loaded unless your browser has no native 91 | WebSockets support or if you force it in the middleware stack: 92 | 93 | ``` ruby 94 | use Rack::LiveReload, force_swf: true 95 | ``` 96 | 97 | If you don't want any of the web-sockets-js code included at all, use the `no_swf` option: 98 | 99 | ``` ruby 100 | use Rack::LiveReload, no_swf: true 101 | ``` 102 | 103 | Once more browsers support WebSockets than don't, this option will be reversed and you'll have 104 | to explicitly include the Flash shim. 105 | 106 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'bundler/setup' 3 | require 'appraisal' 4 | 5 | desc 'Update livereload.js' 6 | task :update_livereload_js do 7 | require 'httparty' 8 | 9 | File.open('js/livereload.js', 'wb') { |fh| 10 | fh.print HTTParty.get('https://raw.github.com/livereload/livereload-js/master/dist/livereload.js').body 11 | } 12 | end 13 | 14 | desc 'Update web-socket-js' 15 | task :update_web_socket_js do 16 | require 'httparty' 17 | 18 | %w{swfobject.js web_socket.js WebSocketMain.swf}.each do |file| 19 | File.open("js/#{file}", 'wb') do |fh| 20 | fh.print HTTParty.get("https://raw.github.com/gimite/web-socket-js/master/#{file}").body 21 | end 22 | end 23 | end 24 | 25 | require 'rspec/core/rake_task' 26 | 27 | RSpec::Core::RakeTask.new(:spec) 28 | 29 | require 'cucumber/rake/task' 30 | 31 | Cucumber::Rake::Task.new(:cucumber) 32 | 33 | task :default => [ :spec, :cucumber ] 34 | 35 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | $: << 'lib' 3 | 4 | require 'rack/livereload' 5 | 6 | use Rack::Logger 7 | use Rack::LiveReload 8 | run Rack::Directory.new('.') 9 | 10 | if false 11 | 12 | get '/' do 13 | File.read('index.html') 14 | end 15 | 16 | run Sinatra::Application 17 | end 18 | -------------------------------------------------------------------------------- /features/skip_certain_browsers.feature: -------------------------------------------------------------------------------- 1 | Feature: Skip Certain Browsers 2 | Scenario Outline: 3 | Given I have a Rack app with Rack::LiveReload 4 | When I make a request to "/" with the following headers: 5 | | HTTP_USER_AGENT | | 6 | Then I should not have any Rack::LiveReload code 7 | 8 | Scenarios: Browsers to check for 9 | | user agent | 10 | | MSIE | 11 | 12 | -------------------------------------------------------------------------------- /features/step_definitions/given/i_have_a_rack_app_with_live_reload.rb: -------------------------------------------------------------------------------- 1 | Given /^I have a Rack app with Rack::LiveReload$/ do 2 | @app = Rack::Builder.new do 3 | use Rack::LiveReload 4 | 5 | run lambda { |env| [ 200, { 'Content-Type' => 'text/html' }, [ "" ] ] } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /features/step_definitions/then/i_should_not_have_livereload_code.rb: -------------------------------------------------------------------------------- 1 | Then /^I should not have any Rack::LiveReload code$/ do 2 | @response.body.should_not include("rack/livereload.js") 3 | end 4 | 5 | -------------------------------------------------------------------------------- /features/step_definitions/when/i_make_a_request_with_headers.rb: -------------------------------------------------------------------------------- 1 | When /^I make a request to "([^"]*)" with the following headers:$/ do |uri, table| 2 | @request = Rack::MockRequest.new(@app) 3 | 4 | @response = @request.get(uri, table.rows_hash) 5 | end 6 | 7 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'rack-livereload' 3 | 4 | -------------------------------------------------------------------------------- /gemfiles/rails32.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 3.2.0" 6 | 7 | gemspec :path=>"../" -------------------------------------------------------------------------------- /gemfiles/rails40.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 4.0.0" 6 | 7 | gemspec :path=>"../" -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | HiRats 3 | -------------------------------------------------------------------------------- /js/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbintz/rack-livereload/8cf0ebec609ee34e781d50c4acb97530838748f4/js/WebSocketMain.swf -------------------------------------------------------------------------------- /js/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 = RACK_LIVERELOAD_PORT; 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 8 | 9 | 10 | <% end %> 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/rack/livereload/body_processor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nokogiri' 3 | 4 | describe Rack::LiveReload::BodyProcessor do 5 | describe 'head tag regex' do 6 | let(:regex) { described_class::HEAD_TAG_REGEX } 7 | subject { regex } 8 | 9 | it { should be_kind_of(Regexp) } 10 | 11 | it 'only picks a valid tag' do 12 | regex.match("").to_s.should eq('') 13 | regex.match("").to_s.should eq('') 14 | regex.match("").to_s.should eq("") 15 | end 16 | 17 | it 'responds false when no head tag' do 18 | regex.match("
").should be_falsey 19 | end 20 | end 21 | 22 | let(:processor) { described_class.new(body, options) } 23 | let(:body) { [ page_html ] } 24 | let(:options) { {} } 25 | let(:page_html) { '' } 26 | 27 | let(:processor_result) do 28 | if !processor.processed? 29 | processor.process!(env) 30 | end 31 | 32 | processor 33 | end 34 | 35 | subject { processor } 36 | 37 | describe "livereload local uri" do 38 | context 'does not exist' do 39 | before do 40 | stub_request(:any, 'localhost:35729/livereload.js').to_timeout 41 | end 42 | 43 | it { should use_vendored } 44 | end 45 | 46 | context 'exists' do 47 | before do 48 | stub_request(:any, 'localhost:35729/livereload.js') 49 | end 50 | 51 | it { should_not use_vendored } 52 | end 53 | 54 | context 'with custom port' do 55 | let(:options) { {:live_reload_port => '12348'}} 56 | 57 | context 'exists' do 58 | before do 59 | stub_request(:any, 'localhost:12348/livereload.js') 60 | end 61 | it { should_not use_vendored } 62 | end 63 | end 64 | 65 | context 'specify vendored' do 66 | let(:options) { { :source => :vendored } } 67 | 68 | it { should use_vendored } 69 | end 70 | 71 | context 'specify LR' do 72 | let(:options) { { :source => :livereload } } 73 | 74 | it { should_not use_vendored } 75 | end 76 | end 77 | 78 | context 'text/html' do 79 | before do 80 | processor.stubs(:use_vendored?).returns(true) 81 | end 82 | 83 | let(:host) { 'host' } 84 | let(:env) { { 'HTTP_HOST' => host } } 85 | 86 | let(:processed_body) { processor_result.new_body.join('') } 87 | let(:length) { processor_result.content_length } 88 | 89 | let(:page_html) { '' } 90 | 91 | context 'vendored' do 92 | it 'should add the vendored livereload js script tag' do 93 | processed_body.should include("script") 94 | processed_body.should include(described_class::LIVERELOAD_JS_PATH) 95 | 96 | length.to_s.should == processed_body.length.to_s 97 | 98 | described_class::LIVERELOAD_JS_PATH.should_not include(host) 99 | 100 | processed_body.should include('swfobject') 101 | processed_body.should include('web_socket') 102 | end 103 | end 104 | 105 | context 'at the top of the head tag' do 106 | let(:page_html) { '' } 107 | 108 | let(:body_dom) { Nokogiri::XML(processed_body) } 109 | 110 | it 'should add the livereload js script tag before all other script tags' do 111 | body_dom.at_css("head")[:attribute].should == 'attribute' 112 | body_dom.at_css("script:eq(5)")[:src].should include(described_class::LIVERELOAD_JS_PATH) 113 | body_dom.at_css("script:last-child")[:insert].should == "before" 114 | end 115 | 116 | context 'when a relative URL root is specified' do 117 | before do 118 | ENV['RAILS_RELATIVE_URL_ROOT'] = '/a_relative_path' 119 | end 120 | 121 | it 'should prepend the relative path to the script src' do 122 | body_dom.at_css("script:eq(5)")[:src].should match(%r{^/a_relative_path/}) 123 | end 124 | end 125 | end 126 | 127 | describe "LIVERELOAD_PORT value" do 128 | let(:options) { { :live_reload_port => 12345 }} 129 | 130 | it "sets the variable at the top of the file" do 131 | processed_body.should include 'RACK_LIVERELOAD_PORT = 12345' 132 | end 133 | end 134 | 135 | context 'in header tags' do 136 | let(:page_html) { "

Just a normal header tag

" } 137 | 138 | let(:body_dom) { Nokogiri::XML(processed_body) } 139 | 140 | it 'should not add the livereload js' do 141 | body_dom.at_css("header")[:class].should == 'hero' 142 | body_dom.css('script').should be_empty 143 | end 144 | end 145 | 146 | context 'not vendored' do 147 | before do 148 | processor.stubs(:use_vendored?).returns(false) 149 | end 150 | 151 | it 'should add the LR livereload js script tag' do 152 | processed_body.should include("script") 153 | processed_body.should include(processor.livereload_local_uri.gsub('localhost', 'host')) 154 | end 155 | end 156 | 157 | context 'set options' do 158 | let(:options) { { :host => new_host, :port => port, :min_delay => min_delay, :max_delay => max_delay } } 159 | let(:min_delay) { 5 } 160 | let(:max_delay) { 10 } 161 | let(:port) { 23 } 162 | let(:new_host) { 'myhost' } 163 | 164 | it 'should add the livereload.js script tag' do 165 | processed_body.should include("mindelay=#{min_delay}") 166 | processed_body.should include("maxdelay=#{max_delay}") 167 | processed_body.should include("port=#{port}") 168 | processed_body.should include("host=#{new_host}") 169 | end 170 | end 171 | 172 | context 'force flash' do 173 | let(:options) { { :force_swf => true } } 174 | 175 | it 'should not add the flash shim' do 176 | processed_body.should include('WEB_SOCKET_FORCE_FLASH') 177 | processed_body.should include('swfobject') 178 | processed_body.should include('web_socket') 179 | end 180 | end 181 | 182 | context 'no flash' do 183 | let(:options) { { :no_swf => true } } 184 | 185 | it 'should not add the flash shim' do 186 | processed_body.should_not include('swfobject') 187 | processed_body.should_not include('web_socket') 188 | end 189 | end 190 | 191 | context 'no host at all' do 192 | let(:env) { {} } 193 | 194 | it 'should use localhost' do 195 | processed_body.should include('localhost') 196 | end 197 | end 198 | end 199 | end 200 | 201 | -------------------------------------------------------------------------------- /spec/rack/livereload/processing_skip_analyzer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Rack::LiveReload::ProcessingSkipAnalyzer do 4 | subject { described_class.new(result, env, options) } 5 | 6 | let(:result) { [ status, headers, body ] } 7 | let(:env) { { 'HTTP_USER_AGENT' => user_agent } } 8 | let(:options) { {} } 9 | 10 | let(:user_agent) { 'Firefox' } 11 | let(:status) { 200 } 12 | let(:headers) { {} } 13 | let(:body) { [] } 14 | 15 | describe '#skip_processing?' do 16 | it "should skip processing" do 17 | subject.skip_processing?.should be_truthy 18 | end 19 | end 20 | 21 | describe '#ignored?' do 22 | let(:options) { { :ignore => [ %r{file} ] } } 23 | 24 | context 'path contains ignore pattern' do 25 | let(:env) { { 'PATH_INFO' => '/this/file', 'QUERY_STRING' => '' } } 26 | 27 | it { should be_ignored } 28 | end 29 | 30 | context 'root path' do 31 | let(:env) { { 'PATH_INFO' => '/', 'QUERY_STRING' => '' } } 32 | 33 | it { should_not be_ignored } 34 | end 35 | end 36 | 37 | describe '#chunked?' do 38 | context 'regular response' do 39 | it { should_not be_chunked } 40 | end 41 | 42 | context 'chunked response' do 43 | let(:headers) { { 'Transfer-Encoding' => 'chunked' } } 44 | 45 | it { should be_chunked } 46 | end 47 | end 48 | 49 | describe '#inline?' do 50 | context 'inline disposition' do 51 | let(:headers) { { 'Content-Disposition' => 'inline; filename=my_inlined_file' } } 52 | 53 | it { should be_inline } 54 | end 55 | end 56 | 57 | describe '#ignored?' do 58 | let(:path_info) { 'path info' } 59 | let(:query_string) { 'query_string' } 60 | let(:env) { { 'PATH_INFO' => path_info, 'QUERY_STRING' => query_string } } 61 | 62 | context 'no ignore set' do 63 | it { should_not be_ignored } 64 | end 65 | 66 | context 'ignore set' do 67 | let(:options) { { :ignore => [ %r{#{path_info}} ] } } 68 | 69 | it { should be_ignored } 70 | end 71 | 72 | context 'ignore set including query_string' do 73 | let(:options) { { :ignore => [ %r{#{path_info}\?#{query_string}} ] } } 74 | 75 | it { should be_ignored } 76 | end 77 | end 78 | 79 | describe '#bad_browser?' do 80 | context 'Firefox' do 81 | it { should_not be_bad_browser } 82 | end 83 | 84 | context 'BAD browser' do 85 | let(:user_agent) { described_class::BAD_USER_AGENTS.first.source } 86 | 87 | it { should be_bad_browser } 88 | end 89 | end 90 | 91 | describe '#html?' do 92 | context 'HTML content' do 93 | let(:headers) { { 'Content-Type' => 'text/html' } } 94 | 95 | it { should be_html } 96 | end 97 | 98 | context 'PDF content' do 99 | let(:headers) { { 'Content-Type' => 'application/pdf' } } 100 | 101 | it { should_not be_html } 102 | end 103 | end 104 | 105 | describe '#get?' do 106 | context 'GET request' do 107 | let(:env) { { 'REQUEST_METHOD' => 'GET' } } 108 | 109 | it { should be_get } 110 | end 111 | 112 | context 'PUT request' do 113 | let(:env) { { 'REQUEST_METHOD' => 'PUT' } } 114 | 115 | it { should_not be_get } 116 | end 117 | 118 | context 'POST request' do 119 | let(:env) { { 'REQUEST_METHOD' => 'POST' } } 120 | 121 | it { should_not be_get } 122 | end 123 | 124 | context 'DELETE request' do 125 | let(:env) { { 'REQUEST_METHOD' => 'DELETE' } } 126 | 127 | it { should_not be_get } 128 | end 129 | 130 | context 'PATCH request' do 131 | let(:env) { { 'REQUEST_METHOD' => 'PATCH' } } 132 | 133 | it { should_not be_get } 134 | end 135 | end 136 | end 137 | 138 | -------------------------------------------------------------------------------- /spec/rack/livereload_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nokogiri' 3 | 4 | describe Rack::LiveReload do 5 | let(:middleware) { described_class.new(app, options) } 6 | let(:app) { stub } 7 | 8 | subject { middleware } 9 | 10 | it 'should be an app' do 11 | middleware.app.should be == app 12 | end 13 | 14 | let(:env) { {} } 15 | let(:options) { {} } 16 | 17 | context '/__rack/livereload.js' do 18 | let(:env) { { 'PATH_INFO' => described_class::BodyProcessor::LIVERELOAD_JS_PATH } } 19 | 20 | before do 21 | middleware.expects(:deliver_file).returns(true) 22 | end 23 | 24 | it 'should return the js file' do 25 | middleware._call(env).should be_truthy 26 | end 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'mocha/api' 2 | require 'webmock/rspec' 3 | 4 | require 'rack-livereload' 5 | 6 | RSpec.configure do |c| 7 | c.mock_with :mocha 8 | end 9 | 10 | module RSpec::Matchers 11 | define :use_vendored do 12 | match do |subject| 13 | subject.use_vendored? 14 | end 15 | end 16 | end 17 | --------------------------------------------------------------------------------