rack-pagespeed 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +59 -0
  3. data/README.md +15 -0
  4. data/Rakefile +36 -0
  5. data/VERSION +1 -0
  6. data/lib/rack-pagespeed.rb +1 -0
  7. data/lib/rack/pagespeed.rb +46 -0
  8. data/lib/rack/pagespeed/config.rb +72 -0
  9. data/lib/rack/pagespeed/filters/all.rb +7 -0
  10. data/lib/rack/pagespeed/filters/base.rb +53 -0
  11. data/lib/rack/pagespeed/filters/combine_css.rb +73 -0
  12. data/lib/rack/pagespeed/filters/combine_javascripts.rb +69 -0
  13. data/lib/rack/pagespeed/filters/inline_css.rb +16 -0
  14. data/lib/rack/pagespeed/filters/inline_javascripts.rb +17 -0
  15. data/lib/rack/pagespeed/filters/minify_javascripts.rb +41 -0
  16. data/lib/rack/pagespeed/store/all.rb +6 -0
  17. data/lib/rack/pagespeed/store/disk.rb +18 -0
  18. data/lib/rack/pagespeed/store/memcached.rb +17 -0
  19. data/spec/config_spec.rb +187 -0
  20. data/spec/filters/combine_css_spec.rb +30 -0
  21. data/spec/filters/combine_javascripts_spec.rb +48 -0
  22. data/spec/filters/filter_spec.rb +57 -0
  23. data/spec/filters/inline_css_spec.rb +33 -0
  24. data/spec/filters/inline_javascript_spec.rb +30 -0
  25. data/spec/filters/minify_javascript_spec.rb +59 -0
  26. data/spec/fixtures/complex.html +33 -0
  27. data/spec/fixtures/foo.js +1 -0
  28. data/spec/fixtures/hh-reset.css +1 -0
  29. data/spec/fixtures/huge.css +1176 -0
  30. data/spec/fixtures/iphone.css +1 -0
  31. data/spec/fixtures/jquery-1.4.1.min.js +152 -0
  32. data/spec/fixtures/medialess1.css +2 -0
  33. data/spec/fixtures/medialess2.css +2 -0
  34. data/spec/fixtures/mylib.js +3 -0
  35. data/spec/fixtures/noexternalcss.html +11 -0
  36. data/spec/fixtures/noscripts.html +9 -0
  37. data/spec/fixtures/ohno.js +1 -0
  38. data/spec/fixtures/reset.css +1 -0
  39. data/spec/fixtures/screen.css +2 -0
  40. data/spec/fixtures/styles.html +10 -0
  41. data/spec/fixtures/zecoolwebsite/css/awesomebydesign.css +94 -0
  42. data/spec/fixtures/zecoolwebsite/css/reset.css +190 -0
  43. data/spec/fixtures/zecoolwebsite/img/bg-idevice.png +0 -0
  44. data/spec/fixtures/zecoolwebsite/img/bg.png +0 -0
  45. data/spec/fixtures/zecoolwebsite/img/bottom-left-arrow.png +0 -0
  46. data/spec/fixtures/zecoolwebsite/img/bottom-right-arrow.png +0 -0
  47. data/spec/fixtures/zecoolwebsite/img/consulting-arrow.png +0 -0
  48. data/spec/fixtures/zecoolwebsite/img/design-arrow.png +0 -0
  49. data/spec/fixtures/zecoolwebsite/img/prototyping-arrow.png +0 -0
  50. data/spec/fixtures/zecoolwebsite/img/top-left-arrow.png +0 -0
  51. data/spec/fixtures/zecoolwebsite/img/top-right-arrow.png +0 -0
  52. data/spec/fixtures/zecoolwebsite/img/webdev-arrow.png +0 -0
  53. data/spec/fixtures/zecoolwebsite/index.html +87 -0
  54. data/spec/fixtures/zecoolwebsite/js/awesomebydesign.js +103 -0
  55. data/spec/fixtures/zecoolwebsite/js/jquery-1.4.2.min.js +154 -0
  56. data/spec/fixtures/zecoolwebsite/js/modernizr-1.5.min.js +28 -0
  57. data/spec/fixtures/zecoolwebsite/js/sayhi.js +1 -0
  58. data/spec/integration/integration_spec.rb +54 -0
  59. data/spec/pagespeed_spec.rb +101 -0
  60. data/spec/spec_helper.rb +54 -0
  61. data/spec/store/disk_spec.rb +34 -0
  62. data/spec/store/memcached_spec.rb +29 -0
  63. metadata +344 -0
@@ -0,0 +1,16 @@
1
+ class Rack::PageSpeed::Filters::InlineCSS < Rack::PageSpeed::Filters::Base
2
+ priority 10
3
+
4
+ def execute! document
5
+ nodes = document.css('link[rel="stylesheet"][href$=".css"]:not([href^="http"])')
6
+ return false unless nodes.count > 0
7
+ nodes.each do |node|
8
+ file = file_for node
9
+ next if file.stat.size > (@options[:max_size] or 2048)
10
+ inline = Nokogiri::XML::Node.new 'style', document
11
+ inline.content = file.read
12
+ node.before inline
13
+ node.remove
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ class Rack::PageSpeed::Filters::InlineJavaScripts < Rack::PageSpeed::Filter
2
+ name 'inline_javascripts'
3
+ priority 10
4
+
5
+ def execute! document
6
+ nodes = document.css('script[src$=".js"]:not([src^="http"])')
7
+ return false unless nodes.count > 0
8
+ nodes.each do |node|
9
+ file = file_for node
10
+ next if file.stat.size > (@options[:max_size] or 2048)
11
+ inline = Nokogiri::XML::Node.new 'script', document
12
+ inline.content = file.read
13
+ node.before inline
14
+ node.remove
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ require 'jsmin'
2
+ begin
3
+ require 'md5'
4
+ rescue LoadError
5
+ require 'digest/md5'
6
+ end
7
+
8
+ class Rack::PageSpeed::Filters::MinifyJavaScripts < Rack::PageSpeed::Filters::Base
9
+ requires_store
10
+ name 'minify_javascripts'
11
+ priority 8
12
+
13
+ def execute! document
14
+ nodes = document.css('script')
15
+ return false unless nodes.count > 0
16
+ nodes.each do |node|
17
+ if !node['src']
18
+ node.content = JSMin.minify node.content
19
+ else
20
+ if match = %r(^/rack-pagespeed-(.*)).match(node['src'])
21
+ store = @options[:store]
22
+ store[match[1]] = JSMin.minify store[match[1]]
23
+ else
24
+ next unless local_script? node
25
+ file = file_for node
26
+ javascript = file.read
27
+ hash = Digest::MD5.hexdigest file.mtime.to_i.to_s + javascript
28
+ compressed = Nokogiri::XML::Node.new 'script', document
29
+ compressed['src'] = "/rack-pagespeed-#{hash}.js"
30
+ @options[:store]["#{hash}.js"] = JSMin.minify javascript
31
+ node.before compressed
32
+ node.remove
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def local_script? node
39
+ node['src'] and !(node['src'] =~ /^http/ or !(node['src'] =~ /.js$/))
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ lib = File.join(File.dirname(__FILE__), '..')
2
+ module Rack::PageSpeed::Store; end
3
+ require "#{lib}/store/disk"
4
+ require "#{lib}/store/memcached"
5
+
6
+
@@ -0,0 +1,18 @@
1
+ require 'tmpdir'
2
+
3
+ class Rack::PageSpeed::Store::Disk
4
+ def initialize path = Dir.tmpdir
5
+ raise ArgumentError, "#{path} is not a directory" unless File.directory? path
6
+ @path = path
7
+ end
8
+
9
+ def [] key
10
+ path = "#{@path}/rack-pagespeed-#{key}"
11
+ File.read path if File.exists? path
12
+ end
13
+
14
+ def []= key, value
15
+ File.open("#{@path}/rack-pagespeed-#{key}", 'w') { |file| file << value }
16
+ true
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ require 'memcached' # wanted to use Dalli, but it just ain't working here right now
2
+
3
+ class Rack::PageSpeed::Store::Memcached
4
+ def initialize address_port = nil
5
+ @client = Memcached.new address_port
6
+ @client.stats # let it raise errors if it can't connect
7
+ end
8
+
9
+ def [] key
10
+ @client.get key
11
+ end
12
+
13
+ def []= key, value
14
+ @client.set key, value
15
+ true
16
+ end
17
+ end
@@ -0,0 +1,187 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe 'rack-pagespeed configuration' do
4
+ before do
5
+ class StripsNaked < Rack::PageSpeed::Filter; end
6
+ class MakesItLookGood < Rack::PageSpeed::Filter; end
7
+ end
8
+
9
+ context 'when instancing a new object' do
10
+ it 'creates methods for each filter class found in Rack::PageSpeed::Filters::Base.available_filters' do
11
+ Rack::PageSpeed::Config.new(:public => Dir.tmpdir).should respond_to :strips_naked
12
+ end
13
+
14
+ it "requires a :public parameter pointing to the app's public directory" do
15
+ expect { Rack::PageSpeed::Config.new("foo" => "bar") }.to raise_error(ArgumentError)
16
+ end
17
+ end
18
+
19
+ context 'sorts filter execution based on their specified order' do
20
+ before do
21
+ class Larry < Rack::PageSpeed::Filter; priority 3; end
22
+ class Moe < Rack::PageSpeed::Filter; priority 2; end
23
+ class Curly < Rack::PageSpeed::Filter; priority 1; end
24
+ @config = Rack::PageSpeed::Config.new :public => Fixtures.path do
25
+ curly
26
+ larry
27
+ moe
28
+ end
29
+ end
30
+
31
+ it "Larry is first" do
32
+ @config.filters.first.should be_a Larry
33
+ end
34
+
35
+ it "Moe is second" do
36
+ @config.filters[1].should be_a Moe
37
+ end
38
+
39
+ it "Curly is last" do
40
+ @config.filters.last.should be_a Curly
41
+ end
42
+ end
43
+
44
+ context 'enabling filters, options hash based' do
45
+ context 'options[:filters]' do
46
+ before { File.stub(:directory?).and_return(true) }
47
+
48
+ it "if it's an array, it enables filters listed in it with their default options" do
49
+ config = Rack::PageSpeed::Config.new :filters => [:makes_it_look_good]
50
+ config.filters.first.should be_a MakesItLookGood
51
+ end
52
+
53
+ it "if it's a hash, it let's you pass options to the filters logically" do
54
+ config = Rack::PageSpeed::Config.new :filters => {:makes_it_look_good => {:test => 6000}}
55
+ filter = config.filters.first
56
+ filter.should be_a MakesItLookGood
57
+ filter.options[:test].should == 6000 # yeah, 2 assertions, bad, bad
58
+ end
59
+ end
60
+
61
+ it 'raises a NoSuchFilterError when a non-existing filter is passed to :filters' do
62
+ expect { Rack::PageSpeed.new page, :filters => [:whoops!] }.to raise_error
63
+ end
64
+ end
65
+
66
+ context 'enabling filters, block/DSL based' do
67
+ before { File.stub(:directory?).and_return(true) }
68
+
69
+ it "let's you invoke filter names in a DSL-like fashion through a block" do
70
+ config = Rack::PageSpeed::Config.new do
71
+ makes_it_look_good
72
+ strips_naked
73
+ end
74
+ config.filters.should include_an_instance_of MakesItLookGood
75
+ config.filters.should include_an_instance_of StripsNaked
76
+ end
77
+
78
+ # two specs below actually work with the optiosn hash based context too
79
+ it "won't add the same filter twice" do
80
+ config = Rack::PageSpeed::Config.new do
81
+ makes_it_look_good
82
+ makes_it_look_good
83
+ strips_naked
84
+ end
85
+ config.filters.count.should == 2
86
+ end
87
+
88
+ it "won't add a filter if it's call returns false" do
89
+ class NeedsStore < Rack::PageSpeed::Filter
90
+ requires_store
91
+ end
92
+ config = Rack::PageSpeed::Config.new do needs_store end
93
+ config.filters.should be_empty
94
+ end
95
+ end
96
+
97
+ context 'setting a storage mechanism' do
98
+ before { File.stub(:directory?).and_return(true) }
99
+
100
+ context 'through the hash options' do
101
+ context ':disk => "directory path" sets to disk storage, with a specific path' do
102
+ before { @config = Rack::PageSpeed::Config.new :store => { :disk => Fixtures.path } }
103
+ subject { @config.store }
104
+ specify { should be_a Rack::PageSpeed::Store::Disk }
105
+ specify { subject.instance_variable_get(:@path).should == Fixtures.path }
106
+ end
107
+ context ":disk sets to disk storage, in the system's temp dir" do
108
+ before { @config = Rack::PageSpeed::Config.new :store => :disk }
109
+ subject { @config.store }
110
+ specify { should be_a Rack::PageSpeed::Store::Disk }
111
+ specify { subject.instance_variable_get(:@path).should == Dir.tmpdir }
112
+ end
113
+ context 'sets to memcache storage if :memcache => "server address/port"' do
114
+ before { @config = Rack::PageSpeed::Config.new :store => { :memcached => 'localhost:11211' } }
115
+ subject { @config.store }
116
+ specify { should be_a Rack::PageSpeed::Store::Memcached }
117
+ specify { subject.instance_variable_get(:@client).servers.first.should =~ /localhost:11211/ }
118
+ end
119
+ context 'sets to memcache storage, 127.0.0.1:11211 if :memcache"' do
120
+ before { @config = Rack::PageSpeed::Config.new :store => :memcached }
121
+ subject { @config.store }
122
+ specify { should be_a Rack::PageSpeed::Store::Memcached }
123
+ specify { subject.instance_variable_get(:@client).servers.first.should =~ /127.0.0.1:11211/ }
124
+ end
125
+ context "raises NoSuchStorageMechanism for weird stuff" do
126
+ specify { expect { Rack::PageSpeed::Config.new :store => :poo }.to raise_error(Rack::PageSpeed::Config::NoSuchStorageMechanism) }
127
+ end
128
+ end
129
+
130
+ context 'through a block passed to the initializer' do
131
+ context 'to a simple Hash, if {} gets passed' do
132
+ before do
133
+ @config = Rack::PageSpeed::Config.new do
134
+ store({})
135
+ end
136
+ end
137
+ subject { @config.store }
138
+ specify { should == {} }
139
+ end
140
+ context 'to disk storage, in a specific path if :disk => "some directory path"' do
141
+ before do
142
+ @config = Rack::PageSpeed::Config.new do
143
+ store :disk => Fixtures.path
144
+ end
145
+ end
146
+ subject { @config.store }
147
+ specify { should be_a Rack::PageSpeed::Store::Disk }
148
+ specify { subject.instance_variable_get(:@path).should == Fixtures.path }
149
+ end
150
+ context ":disk sets to disk storage, in the system's temp dir" do
151
+ before do
152
+ @config = Rack::PageSpeed::Config.new do
153
+ store :disk
154
+ end
155
+ end
156
+ subject { @config.store }
157
+ specify { should be_a Rack::PageSpeed::Store::Disk }
158
+ specify { subject.instance_variable_get(:@path).should == Dir.tmpdir }
159
+ end
160
+ context 'sets to memcache storage if :memcache => "server address/port"' do
161
+ before do
162
+ @config = Rack::PageSpeed::Config.new do
163
+ store :memcached => 'localhost:11211'
164
+ end
165
+ end
166
+ subject { @config.store }
167
+ specify { should be_a Rack::PageSpeed::Store::Memcached }
168
+ specify { subject.instance_variable_get(:@client).servers.first.should =~ /localhost:11211/ }
169
+ end
170
+ context 'sets to memcache storage, localhost:11211 if :memcache"' do
171
+ before do
172
+ @config = Rack::PageSpeed::Config.new do
173
+ store :memcached
174
+ end
175
+ end
176
+ subject { @config.store }
177
+ specify { should be_a Rack::PageSpeed::Store::Memcached }
178
+ specify { subject.instance_variable_get(:@client).servers.first.should =~ /127.0.0.1:11211/ }
179
+ end
180
+ context "raises NoSuchStorageMechanism for weird stuff" do
181
+ specify do
182
+ expect { Rack::PageSpeed::Config.new do store :poo end }.to raise_error(Rack::PageSpeed::Config::NoSuchStorageMechanism)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe 'the combine_css filter' do
4
+ it "is called \"combine_css\" as far as Config is concerned" do
5
+ Rack::PageSpeed::Filters::CombineCSS.name.should == 'combine_css'
6
+ end
7
+
8
+ it "requires a store mechanism to be passed via :store when initializing" do
9
+ Rack::PageSpeed::Filters::CombineCSS.new.should be_false
10
+ end
11
+
12
+ it "is a priority 9 filter" do
13
+ Rack::PageSpeed::Filters::CombineCSS.priority.should == 9
14
+ end
15
+
16
+ context 'execute!' do
17
+ before :each do
18
+ @filter = Rack::PageSpeed::Filters::CombineCSS.new :public => Fixtures.path, :store => {}
19
+ end
20
+
21
+ it 'cuts down the number of scripts in the fixtures from 5 to 1' do
22
+ expect { @filter.execute! Fixtures.complex }.to change { Fixtures.complex.css('link[rel="stylesheet"][href$=".css"]:not([href^="http"])').count }.from(5).to(1)
23
+ end
24
+
25
+ it "stores the nodes' contents in the store passed through the initializer" do
26
+ @filter.instance_variable_get(:@options)[:store].should_receive(:[]=)
27
+ @filter.execute! Fixtures.complex
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe 'the combine_javascripts filter' do
4
+ it "is called \"combine_javascripts\" as far as Config is concerned" do
5
+ Rack::PageSpeed::Filters::CombineJavaScripts.name.should == 'combine_javascripts'
6
+ end
7
+
8
+ it "is a priority 9 filter" do
9
+ Rack::PageSpeed::Filters::CombineJavaScripts.priority 9
10
+ end
11
+
12
+ context "requires a store mechanism to be passed via :store when initializing" do
13
+ specify { Rack::PageSpeed::Filters::CombineJavaScripts.new.should be_false }
14
+ specify { Rack::PageSpeed::Filters::CombineJavaScripts.new(:store => {}).should_not be_false }
15
+ end
16
+
17
+ context 'execute!' do
18
+ before :each do
19
+ @filter = Rack::PageSpeed::Filters::CombineJavaScripts.new :public => Fixtures.path, :store => {}
20
+ end
21
+
22
+ it 'cuts down the number of scripts in the fixtures from 4 to 2' do
23
+ expect { @filter.execute! Fixtures.complex }.to change { Fixtures.complex.css('script[src$=".js"]:not([src^="http"])').count }.from(4).to(2)
24
+ end
25
+
26
+ it "stores the nodes' contents in the store passed through the initializer" do
27
+ @filter.instance_variable_get(:@options)[:store].should_receive(:[]=).twice
28
+ @filter.execute! Fixtures.complex
29
+ end
30
+ end
31
+
32
+ context 'yes, I test private methods, so what?' do
33
+ before do
34
+ @filter = Rack::PageSpeed::Filters::CombineJavaScripts.new :public => Fixtures.path, :store => {}
35
+ end
36
+
37
+ it 'returns an array of arrays containing JS nodes that are next to each other in #group_siblings' do
38
+ nodes = Fixtures.complex.css('script[src$=".js"]:not([src^="http"]) + script[src$=".js"]:not([src^="http"])')
39
+ result = @filter.send :group_siblings, nodes
40
+ result.should == [[nodes[0].previous_element, nodes[0]], [nodes[1].previous_element, nodes[1]]]
41
+ end
42
+
43
+ it '#unique_id a key thats unique to the nodes combination of content + mtime' do
44
+ nodes = Fixtures.complex.css('script[src$=".js"]:not([src^="http"]) + script[src$=".js"]:not([src^="http"])')
45
+ @filter.send(:unique_id, nodes).should == Digest::MD5.hexdigest(nodes.map { |node| file = @filter.send(:file_for, node); file.mtime.to_i.to_s + file.read }.join)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,57 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe 'the base filter class' do
4
+ before { @base = Rack::PageSpeed::Filters::Base.new(:foo => 'bar') }
5
+
6
+ it 'when instancing, it takes an options hash as argument' do
7
+ @base.options[:foo].should == 'bar'
8
+ end
9
+
10
+ it 'when subclassing, it adds the new class to the #available_filters list' do
11
+ class Moo < Rack::PageSpeed::Filters::Base; end
12
+ Rack::PageSpeed::Filters::Base.available_filters.should include(Moo)
13
+ end
14
+
15
+ context 'the #name declaration, which can be used to declare a name which the filter can be called upon' do
16
+ it 'can be called from inside the class' do
17
+ class Boo < Rack::PageSpeed::Filters::Base
18
+ name 'mooers'
19
+ end
20
+ Boo.name.should == 'mooers'
21
+ end
22
+
23
+ it 'defaults to the class name if not called' do
24
+ class BananaSmoothie < Rack::PageSpeed::Filters::Base; end
25
+ BananaSmoothie.name.should == 'banana_smoothie'
26
+ end
27
+ end
28
+
29
+ context 'the #priority declaration, which defines the order that the filter will be executed' do
30
+ it 'takes a number' do
31
+ class NiceFilter < Rack::PageSpeed::Filter
32
+ priority 1
33
+ end
34
+ NiceFilter.priority.should == 1
35
+ end
36
+ it 'takes a string' do
37
+ class NiceFilter < Rack::PageSpeed::Filter
38
+ priority '1'
39
+ end
40
+ NiceFilter.priority.should == 1
41
+ end
42
+ end
43
+
44
+ context '#file_for returns a File object' do
45
+ before { @base.options.stub(:[]).with(:public).and_return(Fixtures.path) }
46
+
47
+ it 'for a script' do
48
+ script = Fixtures.complex.at_css('#mylib')
49
+ @base.send(:file_for, script).stat.size.should == File.size(File.join(Fixtures.path, 'mylib.js'))
50
+ end
51
+
52
+ it "for a stylesheet" do
53
+ style = Fixtures.complex.at_css('link')
54
+ @base.send(:file_for, style).stat.size.should == File.size(File.join(Fixtures.path, 'reset.css'))
55
+ end
56
+ end
57
+ end