asset-trip 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +2 -0
  2. data/History.txt +6 -0
  3. data/MIT-LICENSE.txt +19 -0
  4. data/README.rdoc +27 -0
  5. data/Rakefile +12 -0
  6. data/Thorfile +110 -0
  7. data/asset-trip.gemspec +107 -0
  8. data/init.rb +3 -0
  9. data/lib/asset_trip/asset.rb +82 -0
  10. data/lib/asset_trip/compressor.rb +36 -0
  11. data/lib/asset_trip/config.rb +60 -0
  12. data/lib/asset_trip/file_writer.rb +17 -0
  13. data/lib/asset_trip/helper.rb +83 -0
  14. data/lib/asset_trip/javascript.rb +29 -0
  15. data/lib/asset_trip/load_path.rb +39 -0
  16. data/lib/asset_trip/manifest.rb +40 -0
  17. data/lib/asset_trip/manifest_writer.rb +35 -0
  18. data/lib/asset_trip/memoizable.rb +28 -0
  19. data/lib/asset_trip/middleware.rb +109 -0
  20. data/lib/asset_trip/ssl_stylesheet.rb +15 -0
  21. data/lib/asset_trip/stylesheet.rb +44 -0
  22. data/lib/asset_trip/url_rewriter.rb +82 -0
  23. data/lib/asset_trip.rb +66 -0
  24. data/spec/asset_trip/asset_spec.rb +47 -0
  25. data/spec/asset_trip/compressor_spec.rb +46 -0
  26. data/spec/asset_trip/config_spec.rb +61 -0
  27. data/spec/asset_trip/helper_spec.rb +221 -0
  28. data/spec/asset_trip/javascript_spec.rb +24 -0
  29. data/spec/asset_trip/load_path_spec.rb +61 -0
  30. data/spec/asset_trip/manifest_writer_spec.rb +32 -0
  31. data/spec/asset_trip/middleware_spec.rb +72 -0
  32. data/spec/asset_trip/stylesheet_spec.rb +51 -0
  33. data/spec/asset_trip/url_rewriter_spec.rb +166 -0
  34. data/spec/asset_trip_spec.rb +13 -0
  35. data/spec/fixtures/app/javascripts/main/new.js +1 -0
  36. data/spec/fixtures/app/javascripts/main.js +1 -0
  37. data/spec/fixtures/app/javascripts/signup.js +1 -0
  38. data/spec/fixtures/app/stylesheets/new.css +0 -0
  39. data/spec/fixtures/config/asset_trip/assets.rb +0 -0
  40. data/spec/integration/bundle_spec.rb +243 -0
  41. data/spec/integration/prune_spec.rb +53 -0
  42. data/spec/spec.opts +3 -0
  43. data/spec/spec_helper.rb +24 -0
  44. data/spec/support/core_extensions.rb +13 -0
  45. data/spec/support/helpers.rb +33 -0
  46. data/spec/support/matchers.rb +67 -0
  47. data/spec/support/path_utils.rb +45 -0
  48. data/spec/support/sandbox_helper.rb +19 -0
  49. data/tasks/asset_trip.rake +11 -0
  50. data/vendor/yuicompressor-2.4.2.jar +0 -0
  51. metadata +128 -0
@@ -0,0 +1,35 @@
1
+ module AssetTrip
2
+ class ManifestWriter
3
+
4
+ def initialize(assets)
5
+ @assets = assets
6
+ end
7
+
8
+ def write!
9
+ FileWriter.new(path).write!(contents)
10
+ end
11
+
12
+ private
13
+
14
+ def contents
15
+ source = StringIO.new
16
+
17
+ source.puts "module AssetTrip"
18
+ source.puts " @manifest = Manifest.new"
19
+
20
+ @assets.each do |asset|
21
+ source.puts " @manifest[#{asset.name.inspect}] = #{asset.md5sum.first(11).inspect}"
22
+ end
23
+
24
+ source.puts "end"
25
+
26
+ return source.string
27
+ end
28
+
29
+ def path
30
+ AssetTrip.app_root.join("config", "asset_trip", "manifest.rb")
31
+ end
32
+
33
+ end
34
+ end
35
+
@@ -0,0 +1,28 @@
1
+ module AssetTrip
2
+ module Memoizable
3
+
4
+ def self.memoized_ivar_for(symbol)
5
+ "@_memoized_#{symbol.to_s.sub(/\?\Z/, '_query').sub(/!\Z/, '_bang')}".to_sym
6
+ end
7
+
8
+ def memoize(symbol)
9
+ original_method = :"_unmemoized_#{symbol}"
10
+ memoized_ivar = AssetTrip::Memoizable.memoized_ivar_for(symbol)
11
+
12
+ raise "Already memoized #{symbol}" if method_defined?(original_method)
13
+ alias_method original_method, symbol
14
+
15
+ class_eval <<-EOS, __FILE__, __LINE__
16
+ def #{symbol}(*args) # def mime_type(*args)
17
+ if !defined?(#{memoized_ivar}) # if !defined?(@_memoized_mime_type)
18
+ #{memoized_ivar} = #{original_method}(*args) # @_memoized_mime_type = _unmemoized_mime_type(*args)
19
+ end # end
20
+ #{memoized_ivar} # @_memoized_mime_type
21
+ end # end
22
+ EOS
23
+
24
+ private symbol if private_method_defined?(original_method)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,109 @@
1
+ # This is based on Rack::Static and Rack::File
2
+ module AssetTrip
3
+ class Middleware
4
+ URL_PREFIX = "/__asset_trip__"
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ dup._call(env)
12
+ end
13
+
14
+ def _call(env)
15
+ @env = env
16
+
17
+ if matches_url_prefix?
18
+ process
19
+ else
20
+ pass
21
+ end
22
+ end
23
+
24
+ def each
25
+ File.open(path, "rb") do |file|
26
+ while part = file.read(8192)
27
+ yield part
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def matches_url_prefix?
35
+ path_info.index(URL_PREFIX) == 0
36
+ end
37
+
38
+ def process
39
+ return forbidden if path_info.include?("..")
40
+
41
+ if path.file? && path.readable?
42
+ serve_file
43
+ else
44
+ not_found
45
+ end
46
+ rescue UnknownAssetError
47
+ return not_found
48
+ end
49
+
50
+ def pass
51
+ @app.call(@env)
52
+ end
53
+
54
+ def path
55
+ @path ||= AssetTrip.config.resolve_file(asset_type.to_sym, filename)
56
+ end
57
+
58
+ def filename
59
+ @filename ||= path_info[("/#{URL_PREFIX}/#{asset_type}".size)..-1]
60
+ end
61
+
62
+ def path_info
63
+ @path_info ||= Rack::Utils.unescape(@env["PATH_INFO"])
64
+ end
65
+
66
+ def asset_type
67
+ path_parts[2]
68
+ end
69
+
70
+ def path_parts
71
+ path_info.split("/")
72
+ end
73
+
74
+ def serve_file
75
+ # NOTE:
76
+ # We check via File::size? whether this file provides size info
77
+ # via stat (e.g. /proc files often don't), otherwise we have to
78
+ # figure it out by reading the whole file into memory. And while
79
+ # we're at it we also use this as body then.
80
+ if size = File.size?(@path)
81
+ body = self
82
+ else
83
+ body = [File.read(@path)]
84
+ size = Rack::Utils.bytesize(body.first)
85
+ end
86
+
87
+ [200, {
88
+ "Last-Modified" => File.mtime(@path).httpdate,
89
+ "Content-Type" => Rack::Mime.mime_type(File.extname(@path), 'text/plain'),
90
+ "Content-Length" => size.to_s
91
+ }, body]
92
+ end
93
+
94
+ def forbidden
95
+ body = "Forbidden\n"
96
+ [403, {"Content-Type" => "text/plain",
97
+ "Content-Length" => body.size.to_s},
98
+ [body]]
99
+ end
100
+
101
+ def not_found
102
+ body = "File not found: #{@path_info}\n"
103
+ [404, {"Content-Type" => "text/plain",
104
+ "Content-Length" => body.size.to_s},
105
+ [body]]
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,15 @@
1
+ module AssetTrip
2
+ class SSLStylesheet < Stylesheet
3
+
4
+ def name
5
+ "#{@name}.ssl.css"
6
+ end
7
+
8
+ private
9
+
10
+ def url_rewriter(path)
11
+ UrlRewriter.new("https", path)
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ module AssetTrip
2
+ class Stylesheet < Asset
3
+
4
+ def name
5
+ "#{@name}.css"
6
+ end
7
+
8
+ def ssl_stylesheet
9
+ SSLStylesheet.new(@config, @name, @files)
10
+ end
11
+
12
+ private
13
+
14
+ def joined_contents
15
+ paths.map do |path|
16
+ url_rewriter(path).rewrite(File.read(path))
17
+ end.join("\n\n")
18
+ end
19
+
20
+ def compressor
21
+ Compressor.new("css")
22
+ end
23
+
24
+ def url_rewriter(filesystem_path)
25
+ public_path = AssetTrip.app_root.join("public")
26
+
27
+ if filesystem_path.to_s.starts_with?(public_path)
28
+ foo = Pathname.new("/").join(filesystem_path.relative_path_from(public_path))
29
+ UrlRewriter.new("http", foo)
30
+ else
31
+ UrlRewriter.new("http")
32
+ end
33
+ end
34
+
35
+ def asset_type
36
+ :stylesheets
37
+ end
38
+
39
+ def extension
40
+ ".css"
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,82 @@
1
+ require "action_controller"
2
+ require "action_view"
3
+ require "ostruct"
4
+ require "uri"
5
+
6
+ module AssetTrip
7
+ class UrlRewriter
8
+ include ActionView::Helpers::AssetTagHelper
9
+
10
+ def initialize(scheme, stylesheet_path = nil)
11
+ @scheme = scheme
12
+ @stylesheet_path = stylesheet_path
13
+
14
+ setup_fake_controller_for_asset_host_computation
15
+ end
16
+
17
+ def rewrite(contents)
18
+ contents.gsub(/url *\(([^\)]+)\)/) do
19
+ "url(#{add_asset_host_to_path($1)})"
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def setup_fake_controller_for_asset_host_computation
26
+ environment = {}
27
+ environment["HTTPS"] = "on" if @scheme == "https"
28
+ @controller = OpenStruct.new(:request => ActionController::Request.new(environment)) # Used by Rails compute_asset_host method from ActionView::Helpers::AssetTagHelper
29
+ end
30
+
31
+ def add_asset_host_to_path(path)
32
+ strip_quotes!(path)
33
+
34
+ if prepend_asset_host?(path)
35
+ path = rewrite_relative_path(path) unless @stylesheet_path.blank?
36
+ URI::Generic.build(uri_components(path)).to_s
37
+ else
38
+ path
39
+ end
40
+ end
41
+
42
+ def uri_components(path)
43
+ opts = { :path => path }
44
+
45
+ if (asset_id = rails_asset_id(path)).present?
46
+ opts[:query] = asset_id
47
+ end
48
+
49
+ if (host = compute_asset_host(path)).present?
50
+ opts[:host] = strip_scheme(host)
51
+ opts[:scheme] = @scheme
52
+ end
53
+
54
+ return opts
55
+ end
56
+
57
+ def rewrite_relative_path(relative_url)
58
+ if relative_url.starts_with?("/")
59
+ return relative_url
60
+ else
61
+ Pathname.new(File.join(@stylesheet_path.dirname, relative_url)).cleanpath
62
+ end
63
+ end
64
+
65
+ def prepend_asset_host?(path)
66
+ uri = URI.parse(path)
67
+
68
+ uri.relative? &&
69
+ File.extname(uri.path) != '.htc'
70
+ end
71
+
72
+ def strip_scheme(host)
73
+ host.gsub(/^[a-z]+:\/\//, '')
74
+ end
75
+
76
+ def strip_quotes!(path)
77
+ path.gsub!(/^\s*['"]*/, "")
78
+ path.gsub!(/['"]*\s*$/, "")
79
+ end
80
+
81
+ end
82
+ end
data/lib/asset_trip.rb ADDED
@@ -0,0 +1,66 @@
1
+ require "active_support"
2
+
3
+ module AssetTrip
4
+ autoload :Asset, "asset_trip/asset"
5
+ autoload :Compressor, "asset_trip/compressor"
6
+ autoload :Config, "asset_trip/config"
7
+ autoload :FileWriter, "asset_trip/file_writer"
8
+ autoload :Helper, "asset_trip/helper"
9
+ autoload :Javascript, "asset_trip/javascript"
10
+ autoload :LoadPath, "asset_trip/load_path"
11
+ autoload :Manifest, "asset_trip/manifest"
12
+ autoload :ManifestWriter, "asset_trip/manifest_writer"
13
+ autoload :Middleware, "asset_trip/middleware"
14
+ autoload :Memoizable, "asset_trip/memoizable"
15
+ autoload :SSLStylesheet, "asset_trip/ssl_stylesheet"
16
+ autoload :Stylesheet, "asset_trip/stylesheet"
17
+ autoload :UrlRewriter, "asset_trip/url_rewriter"
18
+
19
+ class CompressorError < StandardError
20
+ end
21
+
22
+ class UnknownAssetError < StandardError
23
+ end
24
+
25
+ class NoManifestError < StandardError
26
+ end
27
+
28
+ VERSION = "0.1.0"
29
+
30
+ mattr_accessor :bundle
31
+ self.bundle = true
32
+
33
+ def self.bundle!
34
+ config.bundle!
35
+ end
36
+
37
+ def self.prune!
38
+ manifest.prune!
39
+ end
40
+
41
+ def self.config
42
+ @config ||= Config.from_file(config_path)
43
+ end
44
+
45
+ def self.manifest
46
+ raise NoManifestError if @manifest.nil?
47
+ @manifest
48
+ end
49
+
50
+ def self.app_root
51
+ Pathname.new(".").expand_path
52
+ end
53
+
54
+ def self.root
55
+ Pathname.new(__FILE__).dirname.join("..").expand_path
56
+ end
57
+
58
+ def self.assets_path
59
+ app_root.join("public", "assets")
60
+ end
61
+
62
+ def self.config_path
63
+ app_root.join("config", "asset_trip")
64
+ end
65
+
66
+ end
@@ -0,0 +1,47 @@
1
+ require "spec_helper"
2
+
3
+ describe AssetTrip::Asset do
4
+ before do
5
+ AssetTrip::Compressor.stub!(:new => DummyCompressor.new)
6
+ File.stub!(:read => "contents")
7
+ end
8
+
9
+ describe "#bundle!" do
10
+ it "splits the MD5 into two nested directories" do
11
+ path = AssetTrip.assets_path.join("d4", "1d8cd98f0", "asset.js")
12
+ AssetTrip::FileWriter.should_receive(:new).with(path).and_return(stub(:write! => nil))
13
+ asset = AssetTrip::Javascript.new(stub, "asset")
14
+ asset.bundle!
15
+ end
16
+
17
+ it "writes the contents to the file" do
18
+ writer = stub
19
+ writer.should_receive(:write!).with("contents")
20
+ AssetTrip::FileWriter.stub!(:new => writer)
21
+ asset = AssetTrip::Javascript.new(stub(:resolve_file => "foo.js"), "asset", [Pathname.new("foo.js")])
22
+ asset.bundle!
23
+ end
24
+ end
25
+
26
+ describe "#md5sum" do
27
+ it "calculates the MD5 if there are no files" do
28
+ asset = AssetTrip::Javascript.new(stub, "asset")
29
+ asset.md5sum.should == "d41d8cd98f00b204e9800998ecf8427e"
30
+ end
31
+
32
+ it "calculates the MD5 for one file" do
33
+ asset = AssetTrip::Javascript.new(stub(:resolve_file => "foo"), "asset") do
34
+ include "foo"
35
+ end
36
+ asset.md5sum.should == "98bf7d8c15784f0a3d63204441e1e2aa"
37
+ end
38
+
39
+ it "calculates the MD5 for multiple file" do
40
+ asset = AssetTrip::Javascript.new(stub(:resolve_file => "foo"), "asset") do
41
+ include "foo"
42
+ include "bar"
43
+ end
44
+ asset.md5sum.should == "237c43254a0799392cac71f795d9250e"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ require "spec_helper"
2
+
3
+ describe AssetTrip::Compressor do
4
+ let(:compressor) do
5
+ AssetTrip::Compressor.new(Pathname.new("css"))
6
+ end
7
+
8
+ describe "#compress" do
9
+ it "builds a command with the proper --type option" do
10
+ POpen4.should_receive(:popen4).with(/--type css/).and_return(stub(:success? => true))
11
+ compressor.compress("a { color: red }")
12
+ end
13
+
14
+ it "builds a command to run the vendored jar file" do
15
+ POpen4.should_receive(:popen4).with(/java -jar .*vendor\/yuicompressor-2.4.2.jar/).and_return(stub(:success? => true))
16
+ compressor.compress("a { color: red }")
17
+ end
18
+
19
+ it "sends the uncompressed contents via STDIN" do
20
+ stdin = stub(:close => nil)
21
+
22
+ POpen4.stub!(:popen4) do |command, block|
23
+ block.call(stub(:read => "STDOUT"), stub(:read => "STDERR"), stdin)
24
+ stub(:success? => true)
25
+ end
26
+
27
+ stdin.should_receive(:puts).with("a { color: red }")
28
+ compressor.compress("a { color: red }").should == "STDOUT"
29
+ end
30
+
31
+ it "returns the STDOUT from the java process" do
32
+ POpen4.stub!(:popen4) do |command, block|
33
+ block.call(stub(:read => "STDOUT"), stub(:read => "STDERR"), stub(:puts => nil, :close => nil))
34
+ stub(:success? => true)
35
+ end
36
+ compressor.compress("a { color: red }").should == "STDOUT"
37
+ end
38
+
39
+ it "raises a CompressorError if the java process is not successful" do
40
+ POpen4.stub!(:popen4 => stub(:success? => false))
41
+ lambda {
42
+ compressor.compress("a { color: red }")
43
+ }.should raise_error(AssetTrip::CompressorError)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,61 @@
1
+ require "spec_helper"
2
+
3
+ describe AssetTrip::Config do
4
+ # TODO: it would be great to avoid needing this for these specs
5
+ setup_sandbox_app!
6
+
7
+ it "can bundle files included with the file extension" do
8
+ config = AssetTrip::Config.new do
9
+ js_asset "signup" do
10
+ include "main.js"
11
+ end
12
+ end
13
+
14
+ asset_config = config.assets.first
15
+ asset_config.paths.should == [app_javascript("main.js")]
16
+ end
17
+
18
+ it "can bundle files in subdirectories of the load path" do
19
+ config = AssetTrip::Config.new do
20
+ js_asset "signup" do
21
+ include "main/new"
22
+ end
23
+ end
24
+
25
+ asset_config = config.assets.first
26
+ asset_config.paths.should == [app_javascript("main", "new.js")]
27
+ end
28
+
29
+ it "supports writing an asset to a subdirectory" do
30
+ config = AssetTrip::Config.new do
31
+ js_asset "signup/foo" do
32
+ include "main.js"
33
+ end
34
+ end
35
+
36
+ asset_config = config.assets.first
37
+ asset_config.name.should == "signup/foo.js"
38
+ end
39
+
40
+ it "supports adding to the JS load paths" do
41
+ config = AssetTrip::Config.new do
42
+ load_paths[:javascripts] << "foo"
43
+ end
44
+
45
+ config.load_paths[:javascripts].should == AssetTrip::LoadPath.new([
46
+ Pathname.new("app/javascripts"),
47
+ Pathname.new("foo")
48
+ ])
49
+ end
50
+
51
+ it "supports adding to the CSS load paths" do
52
+ config = AssetTrip::Config.new do
53
+ load_paths[:stylesheets] << "foo"
54
+ end
55
+
56
+ config.load_paths[:stylesheets].should == AssetTrip::LoadPath.new([
57
+ Pathname.new("app/stylesheets"),
58
+ Pathname.new("foo")
59
+ ])
60
+ end
61
+ end