big refactoring

This commit is contained in:
John Bintz 2013-04-23 16:28:23 -04:00
parent 2eae7fd1bb
commit 730a595be2
7 changed files with 430 additions and 409 deletions

View File

@ -1,50 +1,13 @@
require 'erb'
require 'rack/livereload/processing_skip_analyzer'
require 'rack/livereload/body_processor'
module Rack
class LiveReload
LIVERELOAD_JS_PATH = '/__rack/livereload.js'
HEAD_TAG_REGEX = /<head>|<head[^(er)][^<]*>/
BAD_USER_AGENTS = [ %r{MSIE} ]
def livereload_local_uri
"http://localhost:#{@port}/livereload.js"
end
attr_reader :app
def initialize(app, options = {})
@app, @options = app, options
@port = @options[:live_reload_port] || 35729
end
def use_vendored?
return @use_vendored if @use_vendored
if @options[:source]
@use_vendored = (@options[:source] == :vendored)
else
require 'net/http'
require 'uri'
uri = URI.parse(livereload_local_uri)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1
begin
http.send_request('GET', uri.path)
@use_vendored = false
rescue Timeout::Error, Errno::ECONNREFUSED, EOFError
@use_vendored = true
rescue => e
$stderr.puts e.inspect
raise e
end
end
@use_vendored
end
def call(env)
@ -59,65 +22,21 @@ module Rack
else
status, headers, body = result = @app.call(env)
if (headers['Content-Type'] and (headers['Content-Type']['text/event-stream'] or
headers['Content-Type'][%r{^image/}])) or
headers['Transfer-Encoding'] == 'chunked' or
headers['Content-Disposition'] =~ %r{^inline}
return result
return result if ProcessingSkipAnalyzer.skip_processing?(result, env, @options)
processor = BodyProcessor.new(body, @options)
processor.process!(env)
headers['Content-Length'] = processor.content_length.to_s
if processor.livereload_added
headers['X-Rack-LiveReload'] = '1'
end
body.close if body.respond_to?(:close)
new_body = [] ; body.each { |line| new_body << line.to_s }
if !ignored?(env['PATH_INFO']) and !bad_browser?(env['HTTP_USER_AGENT'])
if headers['Content-Type'] and status == 200 and headers['Content-Type'][%r{text/html}]
content_length = 0
new_body.each do |line|
if !headers['X-Rack-LiveReload'] && line['<head']
host_to_use = (@options[:host] || env['HTTP_HOST'] || 'localhost').gsub(%r{:.*}, '')
app_root = ENV['RAILS_RELATIVE_URL_ROOT'] || ''
if use_vendored?
src = "#{app_root}#{LIVERELOAD_JS_PATH.dup}?host=#{host_to_use}"
else
src = livereload_local_uri.dup.gsub('localhost', host_to_use) + '?'
end
src << "&amp;mindelay=#{@options[:min_delay]}" if @options[:min_delay]
src << "&amp;maxdelay=#{@options[:max_delay]}" if @options[:max_delay]
src << "&amp;port=#{@options[:port]}" if @options[:port]
template = ERB.new(::File.read(::File.expand_path('../../../skel/livereload.html.erb', __FILE__)))
if line['<head']
line.gsub!(HEAD_TAG_REGEX) { |match| %{#{match}#{template.result(binding)}} }
end
headers["X-Rack-LiveReload"] = '1'
end
content_length += line.bytesize
end
headers['Content-Length'] = content_length.to_s
end
end
[ status, headers, new_body ]
[ status, headers, processor.new_body ]
end
end
def ignored?(path_info)
@options[:ignore] and @options[:ignore].any? { |filter| path_info[filter] }
end
def bad_browser?(user_agent)
BAD_USER_AGENTS.any? { |pattern| (user_agent || '')[pattern] }
end
private
def deliver_file(file)
type = case ::File.extname(file)
@ -129,14 +48,6 @@ module Rack
[ 200, { 'Content-Type' => type, 'Content-Length' => ::File.size(file).to_s }, [ ::File.read(file) ] ]
end
def force_swf?
@options[:force_swf]
end
def with_swf?
!@options[:no_swf]
end
end
end

View File

@ -0,0 +1,101 @@
require 'rack/livereload'
module Rack
class LiveReload
class BodyProcessor
LIVERELOAD_JS_PATH = '/__rack/livereload.js'
HEAD_TAG_REGEX = /<head>|<head[^(er)][^<]*>/
LIVERELOAD_PORT = 35729
attr_reader :content_length, :new_body, :livereload_added
def livereload_local_uri
"http://localhost:#{@options[:live_reload_port]}/livereload.js"
end
def initialize(body, options)
@body, @options = body, options
@options[:live_reload_port] ||= LIVERELOAD_PORT
@processed = false
end
def force_swf?
@options[:force_swf]
end
def with_swf?
!@options[:no_swf]
end
def use_vendored?
return @use_vendored if @use_vendored
if @options[:source]
@use_vendored = (@options[:source] == :vendored)
else
require 'net/http'
require 'uri'
uri = URI.parse(livereload_local_uri)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1
begin
http.send_request('GET', uri.path)
@use_vendored = false
rescue Timeout::Error, Errno::ECONNREFUSED, EOFError
@use_vendored = true
rescue => e
$stderr.puts e.inspect
raise e
end
end
@use_vendored
end
def processed?
@processed
end
def process!(env)
@body.close if @body.respond_to?(:close)
@new_body = [] ; @body.each { |line| @new_body << line.to_s }
@content_length = 0
@livereload_added = false
@new_body.each do |line|
if !@livereload_added && line['<head']
host_to_use = (@options[:host] || env['HTTP_HOST'] || 'localhost').gsub(%r{:.*}, '')
app_root = ENV['RAILS_RELATIVE_URL_ROOT'] || ''
if use_vendored?
src = "#{app_root}#{LIVERELOAD_JS_PATH.dup}?host=#{host_to_use}"
else
src = livereload_local_uri.dup.gsub('localhost', host_to_use) + '?'
end
src << "&amp;mindelay=#{@options[:min_delay]}" if @options[:min_delay]
src << "&amp;maxdelay=#{@options[:max_delay]}" if @options[:max_delay]
src << "&amp;port=#{@options[:port]}" if @options[:port]
template = ERB.new(::File.read(::File.expand_path('../../../../skel/livereload.html.erb', __FILE__)))
line.gsub!(HEAD_TAG_REGEX) { |match| %{#{match}#{template.result(binding)}} }
@livereload_added = true
end
@content_length += line.bytesize
@processed = true
end
end
end
end
end

View File

@ -0,0 +1,44 @@
require 'rack/livereload'
module Rack
class LiveReload
class ProcessingSkipAnalyzer
BAD_USER_AGENTS = [ %r{MSIE} ]
def self.skip_processing?(result, env, options)
new(result, env, options).skip_processing?
end
def initialize(result, env, options)
@env, @options = env, options
@status, @headers, @body = result
end
def skip_processing?
!html? || event_stream? || chunked? || inline? || ignored? || bad_browser?
end
def chunked?
@headers['Transfer-Encoding'] == 'chunked'
end
def inline?
@headers['Content-Disposition'] =~ %r{^inline}
end
def ignored?
@options[:ignore] and @options[:ignore].any? { |filter| @env['PATH_INFO'][filter] }
end
def bad_browser?
BAD_USER_AGENTS.any? { |pattern| @env['HTTP_USER_AGENT'] =~ pattern }
end
def html?
@headers['Content-Type'] =~ %r{text/html} and @status == 200
end
end
end
end

View File

@ -9,7 +9,7 @@
<script type="text/javascript" src="<%= app_root %>/__rack/web_socket.js"></script>
<% end %>
<script type="text/javascript">
RACK_LIVERELOAD_PORT = <%= @port %>;
RACK_LIVERELOAD_PORT = <%= @options[:live_reload_port] %>;
</script>
<script type="text/javascript" src="<%= src %>"></script>

View File

@ -0,0 +1,200 @@
require 'spec_helper'
require 'nokogiri'
describe Rack::LiveReload::BodyProcessor do
describe 'head tag regex' do
let(:regex) { described_class::HEAD_TAG_REGEX }
subject { regex }
it { should be_kind_of(Regexp) }
it 'only picks a valid <head> tag' do
regex.match("<head></head>").to_s.should eq('<head>')
regex.match("<head><title></title></head>").to_s.should eq('<head>')
regex.match("<head attribute='something'><title></title></head>").to_s.should eq("<head attribute='something'>")
end
it 'responds false when no head tag' do
regex.match("<header></header>").should be_false
end
end
let(:processor) { described_class.new(body, options) }
let(:body) { [ page_html ] }
let(:options) { {} }
let(:page_html) { '<head></head>' }
let(:processor_result) do
if !processor.processed?
processor.process!(env)
end
processor
end
subject { processor }
describe "livereload local uri" do
context 'does not exist' do
before do
stub_request(:any, 'localhost:35729/livereload.js').to_timeout
end
it { should use_vendored }
end
context 'exists' do
before do
stub_request(:any, 'localhost:35729/livereload.js')
end
it { should_not use_vendored }
end
context 'with custom port' do
let(:options) { {:live_reload_port => '12348'}}
context 'exists' do
before do
stub_request(:any, 'localhost:12348/livereload.js')
end
it { should_not use_vendored }
end
end
context 'specify vendored' do
let(:options) { { :source => :vendored } }
it { should use_vendored }
end
context 'specify LR' do
let(:options) { { :source => :livereload } }
it { should_not use_vendored }
end
end
context 'text/html' do
before do
processor.stubs(:use_vendored?).returns(true)
end
let(:host) { 'host' }
let(:env) { { 'HTTP_HOST' => host } }
let(:processed_body) { processor_result.new_body.join('') }
let(:length) { processor_result.content_length }
let(:page_html) { '<head></head>' }
context 'vendored' do
it 'should add the vendored livereload js script tag' do
processed_body.should include("script")
processed_body.should include(described_class::LIVERELOAD_JS_PATH)
length.to_s.should == processed_body.length.to_s
described_class::LIVERELOAD_JS_PATH.should_not include(host)
processed_body.should include('swfobject')
processed_body.should include('web_socket')
end
end
context 'at the top of the head tag' do
let(:page_html) { '<head attribute="attribute"><script type="text/javascript" insert="first"></script><script type="text/javascript" insert="before"></script></head>' }
let(:body_dom) { Nokogiri::XML(processed_body) }
it 'should add the livereload js script tag before all other script tags' do
body_dom.at_css("head")[:attribute].should == 'attribute'
body_dom.at_css("script:eq(5)")[:src].should include(described_class::LIVERELOAD_JS_PATH)
body_dom.at_css("script:last-child")[:insert].should == "before"
end
context 'when a relative URL root is specified' do
before do
ENV['RAILS_RELATIVE_URL_ROOT'] = '/a_relative_path'
end
it 'should prepend the relative path to the script src' do
body_dom.at_css("script:eq(5)")[:src].should match(%r{^/a_relative_path/})
end
end
end
describe "LIVERELOAD_PORT value" do
let(:options) { { :live_reload_port => 12345 }}
it "sets the variable at the top of the file" do
processed_body.should include 'RACK_LIVERELOAD_PORT = 12345'
end
end
context 'in header tags' do
let(:page_html) { "<header class='hero'><h1>Just a normal header tag</h1></header>" }
let(:body_dom) { Nokogiri::XML(processed_body) }
it 'should not add the livereload js' do
body_dom.at_css("header")[:class].should == 'hero'
body_dom.css('script').should be_empty
end
end
context 'not vendored' do
before do
processor.stubs(:use_vendored?).returns(false)
end
it 'should add the LR livereload js script tag' do
processed_body.should include("script")
processed_body.should include(processor.livereload_local_uri.gsub('localhost', 'host'))
end
end
context 'set options' do
let(:options) { { :host => new_host, :port => port, :min_delay => min_delay, :max_delay => max_delay } }
let(:min_delay) { 5 }
let(:max_delay) { 10 }
let(:port) { 23 }
let(:new_host) { 'myhost' }
it 'should add the livereload.js script tag' do
processed_body.should include("mindelay=#{min_delay}")
processed_body.should include("maxdelay=#{max_delay}")
processed_body.should include("port=#{port}")
processed_body.should include("host=#{new_host}")
end
end
context 'force flash' do
let(:options) { { :force_swf => true } }
it 'should not add the flash shim' do
processed_body.should include('WEB_SOCKET_FORCE_FLASH')
processed_body.should include('swfobject')
processed_body.should include('web_socket')
end
end
context 'no flash' do
let(:options) { { :no_swf => true } }
it 'should not add the flash shim' do
processed_body.should_not include('swfobject')
processed_body.should_not include('web_socket')
end
end
context 'no host at all' do
let(:env) { {} }
it 'should use localhost' do
processed_body.should include('localhost')
end
end
end
end

View File

@ -0,0 +1,71 @@
require 'spec_helper'
describe Rack::LiveReload::ProcessingSkipAnalyzer do
subject { described_class.new(result, env, options) }
let(:result) { [ status, headers, body ] }
let(:env) { { 'HTTP_USER_AGENT' => user_agent } }
let(:options) { {} }
let(:user_agent) { 'Firefox' }
let(:status) { 200 }
let(:headers) { {} }
let(:body) { [] }
describe '#bad_browser?' do
let(:user_agent) { described_class::BAD_USER_AGENTS.first.source }
it { should be_bad_browser }
end
context 'ignored' do
let(:options) { { :ignore => [ %r{file} ] } }
context 'not root' do
let(:env) { { 'PATH_INFO' => '/this/file' } }
it { should be_ignored }
end
context 'root' do
let(:env) { { 'PATH_INFO' => '/' } }
it { should_not be_ignored }
end
end
context 'not text/html' do
let(:headers) { { 'Content-Type' => 'application/pdf' } }
it { should_not be_html }
end
context 'chunked response' do
let(:headers) { { 'Transfer-Encoding' => 'chunked' } }
it { should be_chunked }
end
context 'inline disposition' do
let(:headers) { { 'Content-Disposition' => 'inline; filename=my_inlined_file' } }
it { should be_inline }
end
describe '#ignored?' do
let(:path_info) { 'path info' }
let(:env) { { 'PATH_INFO' => path_info } }
context 'no ignore set' do
it { should_not be_ignored }
end
context 'ignore set' do
let(:options) { { :ignore => [ %r{#{path_info}} ] } }
it { should be_ignored }
end
end
end

View File

@ -12,277 +12,8 @@ describe Rack::LiveReload do
let(:env) { {} }
let(:options) { {} }
describe "livereload local uri" do
context 'does not exist' do
before do
stub_request(:any, 'localhost:35729/livereload.js').to_timeout
end
it { should use_vendored }
end
context 'exists' do
before do
stub_request(:any, 'localhost:35729/livereload.js')
end
it { should_not use_vendored }
end
context 'with custom port' do
let(:options) { {:live_reload_port => '12348'}}
context 'exists' do
before do
stub_request(:any, 'localhost:12348/livereload.js')
end
it { should_not use_vendored }
end
end
context 'specify vendored' do
let(:options) { { :source => :vendored } }
it { should use_vendored }
end
context 'specify LR' do
let(:options) { { :source => :livereload } }
it { should_not use_vendored }
end
end
context 'not text/html' do
let(:ret) { [ 200, { 'Content-Type' => 'application/pdf' }, [ '<head></head>' ] ] }
before do
app.stubs(:call).with(env).returns(ret)
end
it 'should pass through' do
middleware.call(env).should == ret
end
end
context 'image/png' do
let(:body) { [ '<head></head>' ] }
let(:ret) { [ 200, { 'Content-Type' => 'image/png' }, body ] }
before do
app.stubs(:call).with(env).returns(ret)
body.expects(:close).never
body.stubs(:respond_to?).with(:close).returns(true)
end
it 'should pass through' do
middleware.call(env).should == ret
end
end
context 'text/event-stram' do
let(:body) { [ '<head></head>' ] }
let(:ret) { [ 200, { 'Content-Type' => 'text/event-stream' }, body ] }
before do
app.stubs(:call).with(env).returns(ret)
body.expects(:close).never
body.stubs(:respond_to?).with(:close).returns(true)
end
it 'should pass through' do
middleware.call(env).should == ret
end
end
context 'chunked response' do
let(:body) { [ '<head></head>' ] }
let(:ret) { [ 200, { 'Transfer-Encoding' => 'chunked' }, body ] }
before do
app.stubs(:call).with(env).returns(ret)
body.expects(:close).never
body.stubs(:respond_to?).with(:close).returns(true)
end
it 'should pass through' do
middleware.call(env).should == ret
end
end
context 'inline disposition' do
let(:body) { [ '<head></head>' ] }
let(:ret) { [ 200, { 'Content-Disposition' => 'inline; filename=my_inlined_file' }, body ] }
before do
app.stubs(:call).with(env).returns(ret)
body.expects(:close).never
body.stubs(:respond_to?).with(:close).returns(true)
end
it 'should pass through' do
middleware.call(env).should == ret
end
end
context 'unknown Content-Type' do
let(:ret) { [ 200, {}, [ 'hey ho' ] ] }
before do
app.stubs(:call).with(env).returns(ret)
end
it 'should not break' do
middleware.call(env).should_not raise_error(NoMethodError, /You have a nil object/)
end
end
context 'text/html' do
before do
app.stubs(:call).with(env).returns([ 200, { 'Content-Type' => 'text/html', 'Content-Length' => 0 }, [ page_html ] ])
middleware.stubs(:use_vendored?).returns(true)
end
let(:host) { 'host' }
let(:env) { { 'HTTP_HOST' => host } }
let(:ret) { middleware._call(env) }
let(:body) { ret.last.join }
let(:length) { ret[1]['Content-Length'] }
let(:page_html) { '<head></head>' }
context 'vendored' do
it 'should add the vendored livereload js script tag' do
body.should include("script")
body.should include(described_class::LIVERELOAD_JS_PATH)
length.should == body.length.to_s
described_class::LIVERELOAD_JS_PATH.should_not include(host)
body.should include('swfobject')
body.should include('web_socket')
end
end
context 'at the top of the head tag' do
let(:page_html) { '<head attribute="attribute"><script type="text/javascript" insert="first"></script><script type="text/javascript" insert="before"></script></head>' }
let(:body_dom) { Nokogiri::XML(body) }
it 'should add the livereload js script tag before all other script tags' do
body_dom.at_css("head")[:attribute].should == 'attribute'
body_dom.at_css("script:eq(5)")[:src].should include(described_class::LIVERELOAD_JS_PATH)
body_dom.at_css("script:last-child")[:insert].should == "before"
end
context 'when a relative URL root is specified' do
before do
ENV['RAILS_RELATIVE_URL_ROOT'] = '/a_relative_path'
end
it 'should prepend the relative path to the script src' do
body_dom.at_css("script:eq(5)")[:src].should match(%r{^/a_relative_path/})
end
end
end
describe "LIVERELOAD_PORT value" do
let(:options) { {:live_reload_port => 12345 }}
it "sets the variable at the top of the file" do
body.should include 'RACK_LIVERELOAD_PORT = 12345'
end
end
context 'in header tags' do
let(:page_html) { "<header class='hero'><h1>Just a normal header tag</h1></header>" }
let(:body_dom) { Nokogiri::XML(body) }
it 'should not add the livereload js' do
body_dom.at_css("header")[:class].should == 'hero'
body_dom.css('script').should be_empty
end
end
context 'not vendored' do
before do
middleware.stubs(:use_vendored?).returns(false)
end
it 'should add the LR livereload js script tag' do
body.should include("script")
body.should include(middleware.livereload_local_uri.gsub('localhost', 'host'))
end
end
context 'set options' do
let(:middleware) { described_class.new(app, :host => new_host, :port => port, :min_delay => min_delay, :max_delay => max_delay) }
let(:min_delay) { 5 }
let(:max_delay) { 10 }
let(:port) { 23 }
let(:new_host) { 'myhost' }
it 'should add the livereload.js script tag' do
body.should include("mindelay=#{min_delay}")
body.should include("maxdelay=#{max_delay}")
body.should include("port=#{port}")
body.should include("host=#{new_host}")
end
end
context 'force flash' do
let(:middleware) { described_class.new(app, :force_swf => true) }
it 'should not add the flash shim' do
body.should include('WEB_SOCKET_FORCE_FLASH')
body.should include('swfobject')
body.should include('web_socket')
end
end
context 'no flash' do
let(:middleware) { described_class.new(app, :no_swf => true) }
it 'should not add the flash shim' do
body.should_not include('swfobject')
body.should_not include('web_socket')
end
end
context 'no host at all' do
let(:env) { {} }
it 'should use localhost' do
body.should include('localhost')
end
end
context 'ignored' do
let(:options) { { :ignore => [ %r{file} ] } }
context 'not root' do
let(:env) { { 'PATH_INFO' => '/this/file' } }
it 'should have no change' do
body.should_not include('script')
end
end
context 'root' do
let(:env) { { 'PATH_INFO' => '/' } }
it 'should have script' do
body.should include('script')
end
end
end
end
context '/__rack/livereload.js' do
let(:env) { { 'PATH_INFO' => described_class::LIVERELOAD_JS_PATH } }
let(:env) { { 'PATH_INFO' => described_class::BodyProcessor::LIVERELOAD_JS_PATH } }
before do
middleware.expects(:deliver_file).returns(true)
@ -292,42 +23,5 @@ describe Rack::LiveReload do
middleware._call(env).should be_true
end
end
describe '#ignored?' do
let(:path_info) { 'path info' }
context 'no ignore set' do
it { should_not be_ignored(path_info) }
end
context 'ignore set' do
let(:options) { { :ignore => [ %r{#{path_info}} ] } }
it { should be_ignored(path_info) }
end
end
describe '#bad_browser?' do
let(:user_agent) { described_class::BAD_USER_AGENTS.first.source }
it { should be_bad_browser(user_agent) }
end
describe 'head tag regex' do
let(:regex) { described_class::HEAD_TAG_REGEX }
subject { regex }
it { should be_kind_of(Regexp) }
it 'only picks a valid <head> tag' do
regex.match("<head></head>").to_s.should eq('<head>')
regex.match("<head><title></title></head>").to_s.should eq('<head>')
regex.match("<head attribute='something'><title></title></head>").to_s.should eq("<head attribute='something'>")
end
it 'responds false when no head tag' do
regex.match("<header></header>").should be_false
end
end
end