asset-trip 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 (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