sinatra-assetpack 0.0.5

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 (55) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +2 -0
  3. data/HISTORY.md +45 -0
  4. data/README.md +248 -0
  5. data/Rakefile +3 -0
  6. data/example/.gitignore +1 -0
  7. data/example/Rakefile +7 -0
  8. data/example/app.rb +28 -0
  9. data/example/app/css/test.sass +11 -0
  10. data/example/app/images/icon.png +0 -0
  11. data/example/app/js/app.js +3 -0
  12. data/example/app/js/vendor/jquery.js +2 -0
  13. data/example/app/js/vendor/jquery.plugin.js +2 -0
  14. data/example/app/js/vendor/underscore.js +2 -0
  15. data/example/views/index.erb +26 -0
  16. data/lib/sinatra/assetpack.rb +55 -0
  17. data/lib/sinatra/assetpack/buster_helpers.rb +21 -0
  18. data/lib/sinatra/assetpack/class_methods.rb +68 -0
  19. data/lib/sinatra/assetpack/compressor.rb +109 -0
  20. data/lib/sinatra/assetpack/configurator.rb +15 -0
  21. data/lib/sinatra/assetpack/css.rb +35 -0
  22. data/lib/sinatra/assetpack/hasharray.rb +70 -0
  23. data/lib/sinatra/assetpack/helpers.rb +48 -0
  24. data/lib/sinatra/assetpack/html_helpers.rb +17 -0
  25. data/lib/sinatra/assetpack/image.rb +37 -0
  26. data/lib/sinatra/assetpack/options.rb +223 -0
  27. data/lib/sinatra/assetpack/package.rb +111 -0
  28. data/lib/sinatra/assetpack/rake.rb +23 -0
  29. data/lib/sinatra/assetpack/version.rb +7 -0
  30. data/sinatra-assetpack.gemspec +23 -0
  31. data/test/app/.gitignore +1 -0
  32. data/test/app/Rakefile +7 -0
  33. data/test/app/app.rb +51 -0
  34. data/test/app/app/css/js2c.css +494 -0
  35. data/test/app/app/css/screen.sass +9 -0
  36. data/test/app/app/css/sqwishable.css +7 -0
  37. data/test/app/app/css/style.css +2 -0
  38. data/test/app/app/images/background.jpg +1 -0
  39. data/test/app/app/images/email.png +0 -0
  40. data/test/app/app/js/hello.js +1 -0
  41. data/test/app/app/js/hi.coffee +2 -0
  42. data/test/app/app/views/index.haml +1 -0
  43. data/test/build_test.rb +20 -0
  44. data/test/cache_test.rb +10 -0
  45. data/test/helpers_test.rb +23 -0
  46. data/test/img_test.rb +13 -0
  47. data/test/options_test.rb +17 -0
  48. data/test/order_test.rb +21 -0
  49. data/test/preproc_test.rb +23 -0
  50. data/test/simplecss_test.rb +16 -0
  51. data/test/sqwish_test.rb +29 -0
  52. data/test/test_helper.rb +38 -0
  53. data/test/unit_test.rb +96 -0
  54. data/test/yui_test.rb +22 -0
  55. metadata +200 -0
@@ -0,0 +1,21 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ module BusterHelpers
4
+ extend self
5
+ # Returns the cache buster suffix for given file(s).
6
+ # This implementation somewhat obfuscates the mtime to not reveal deployment dates.
7
+ def cache_buster_hash(*files)
8
+ i = files.map { |f| File.mtime(f).to_i }.max
9
+ (i * 4567).to_s.reverse[0...6]
10
+ end
11
+
12
+ # Adds a cache buster for the given path.
13
+ #
14
+ # add_cache_buster('/images/email.png', '/var/www/x/public/images/email.png')
15
+ #
16
+ def add_cache_buster(path, *files)
17
+ path.gsub(/(\.[^.]+)$/) { |ext| ".#{cache_buster_hash *files}#{ext}" }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,68 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ # Class methods that will be given to the Sinatra application.
4
+ module ClassMethods
5
+ # Sets asset options, or gets them
6
+ def assets(&blk)
7
+ @options ||= Options.new(self, &blk)
8
+ self.assets_initialize! if block_given?
9
+
10
+ @options
11
+ end
12
+
13
+ def assets_initialize!
14
+ add_compressed_routes!
15
+ add_individual_routes!
16
+ end
17
+
18
+ # Add routes for the compressed versions
19
+ def add_compressed_routes!
20
+ assets.packages.each do |name, package|
21
+ get package.route_regex do
22
+ content_type package.type
23
+ last_modified package.mtime
24
+
25
+ settings.assets.cache[package.hash] ||= package.minify
26
+ end
27
+ end
28
+ end
29
+
30
+ # Add the routes for the individual files.
31
+ def add_individual_routes!
32
+ assets.served.each do |path, from|
33
+ get "/#{path}/*".squeeze('/') do |file|
34
+ fmt = File.extname(file)[1..-1]
35
+
36
+ # Sanity checks
37
+ pass unless AssetPack.supported_formats.include?(fmt)
38
+ fn = asset_path_for(file, from) or pass
39
+
40
+ # Send headers
41
+ content_type fmt.to_sym
42
+ last_modified File.mtime(fn).to_i
43
+
44
+ format = File.extname(fn)[1..-1]
45
+
46
+ if AssetPack.supported_formats.include?(format)
47
+ # It's a raw file, just send it
48
+ not_found unless format == fmt
49
+
50
+ if fmt == 'css'
51
+ asset_filter_css File.read(fn)
52
+ else
53
+ send_file fn
54
+ end
55
+ else
56
+ # Dynamic file
57
+ not_found unless AssetPack.tilt_formats[format] == fmt
58
+ out = render format.to_sym, File.read(fn)
59
+ out = asset_filter_css(out) if fmt == 'css'
60
+ out
61
+ end
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,109 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ module Compressor
4
+ extend self
5
+
6
+ # Compresses a given string.
7
+ #
8
+ # compress File.read('x.js'), :js, :jsmin
9
+ #
10
+ def compress(str, type, engine=nil, options={})
11
+ engine ||= 'jsmin' if type == :js
12
+ engine ||= 'simple' if type == :css
13
+
14
+ key = :"#{type}/#{engine}"
15
+ meth = compressors[key]
16
+ return str unless meth
17
+
18
+ meth[str, options]
19
+ end
20
+
21
+ def compressors
22
+ @compressors ||= {
23
+ :'js/jsmin' => method(:jsmin),
24
+ :'js/yui' => method(:yui_js),
25
+ :'js/closure' => method(:closure_js),
26
+ :'css/sass' => method(:sass),
27
+ :'css/yui' => method(:yui_css),
28
+ :'css/simple' => method(:simple_css),
29
+ :'css/sqwish' => method(:sqwish_css)
30
+ }
31
+ end
32
+
33
+ # =====================================================================
34
+ # Compressors
35
+
36
+ def jsmin(str, options={})
37
+ require 'jsmin'
38
+ JSMin.minify str
39
+ end
40
+
41
+ def sass(str, options={})
42
+ Tilt.new("scss", {:style => :compressed}) { str }.render
43
+ rescue LoadError
44
+ simple_css str
45
+ end
46
+
47
+ def yui_css(str, options={})
48
+ require 'yui/compressor'
49
+ YUI::CssCompressor.new.compress(str)
50
+ rescue Errno::ENOENT
51
+ sass str
52
+ end
53
+
54
+ def yui_js(str, options={})
55
+ require 'yui/compressor'
56
+ YUI::JavaScriptCompressor.new(options).compress(str)
57
+ rescue LoadError
58
+ jsmin str
59
+ end
60
+
61
+ def simple_css(str, options={})
62
+ str.gsub! /[ \r\n\t]+/m, ' '
63
+ str.gsub! %r{ *([;\{\},:]) *}, '\1'
64
+ end
65
+
66
+ def sqwish_css(str, options={})
67
+ cmd = "#{sqwish_bin} %f "
68
+ cmd += "--strict" if options[:strict]
69
+
70
+ _, input = sys :css, str, cmd
71
+ output = input.gsub(/\.css/, '.min.css')
72
+
73
+ File.read(output)
74
+ rescue => e
75
+ simple_css str
76
+ end
77
+
78
+ def sqwish_bin
79
+ ENV['SQWISH_PATH'] || "sqwish"
80
+ end
81
+
82
+ def closure_js(str, options={})
83
+ require 'net/http'
84
+ require 'uri'
85
+
86
+ response = Net::HTTP.post_form(URI.parse('http://closure-compiler.appspot.com/compile'), {
87
+ 'js_code' => str,
88
+ 'compilation_level' => options[:level] || "ADVANCED_OPTIMIZATIONS",
89
+ 'output_format' => 'text',
90
+ 'output_info' => 'compiled_code'
91
+ })
92
+
93
+ response.body
94
+ end
95
+
96
+ # For others
97
+ def sys(type, str, cmd)
98
+ t = Tempfile.new ['', ".#{type}"]
99
+ t.write(str)
100
+ t.close
101
+
102
+ output = `#{cmd.gsub('%f', t.path)}`
103
+ FileUtils.rm t
104
+
105
+ [output, t.path]
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,15 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ module Configurator
4
+ def attrib(name)
5
+ define_method(:"#{name}") { |*a|
6
+ value = a.first
7
+ self.instance_variable_set :"@#{name}", value unless value.nil?
8
+ self.instance_variable_get :"@#{name}"
9
+ }
10
+
11
+ alias_method(:"#{name}=", :"#{name}")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ module Css
4
+ def self.preproc(str, assets)
5
+ str.gsub(/url\(["']?(.*?)["']?\)/) { |url|
6
+ file, options = $1.split('?')
7
+ local = assets.local_file_for file
8
+
9
+ url = if local
10
+ if options.to_s.include?('embed')
11
+ to_data_uri(local)
12
+ else
13
+ BusterHelpers.add_cache_buster(file, local)
14
+ end
15
+ else
16
+ url
17
+ end
18
+
19
+ "url(#{url})"
20
+ }
21
+ end
22
+
23
+ def self.to_data_uri(file)
24
+ require 'base64'
25
+
26
+ data = File.read(file)
27
+ ext = File.extname(file)
28
+ mime = Sinatra::Base.mime_type(ext)
29
+ b64 = Base64.encode64(data).gsub("\n", '')
30
+
31
+ "data:#{mime};base64,#{b64}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,70 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ # Class: HashArray
4
+ # A stopgap solution to Ruby 1.8's lack of ordered hashes.
5
+ #
6
+ # A HashArray, for all intents and purposes, acts like an array. However, the
7
+ # common stuff are overloaded to work with hashes.
8
+ #
9
+ # ## Basic usage
10
+ #
11
+ # #### Creating
12
+ # You can create a HashArray by passing it an array.
13
+ #
14
+ # dict = HashArray.new([
15
+ # { :good_morning => "Bonjour" },
16
+ # { :goodbye => "Au revoir" },
17
+ # { :good_evening => "Bon nuit" }
18
+ # ])
19
+ #
20
+ # #### Converting
21
+ # You may also use it like so:
22
+ #
23
+ # letters = [ { :a => "Aye"}, { :b => "Bee" } ].to_hash_array
24
+ #
25
+ # #### Iterating
26
+ # Now you can use the typical enumerator functions:
27
+ #
28
+ # dict.each do |(key, value)|
29
+ # puts "#{key} is #{value}"
30
+ # end
31
+ #
32
+ # #=> :good_morning is "Bonjour"
33
+ # # :goodbye is "Au revoir"
34
+ # # :good_evening is "Bon nuit"
35
+ #
36
+ class HashArray < Array
37
+ def self.[](*arr)
38
+ new arr.each_slice(2).map { |(k, v)| Hash[k, v] }
39
+ end
40
+
41
+ # Works like Hash#each.
42
+ #
43
+ # By extension, methods that rely on #each (like #inject) will work
44
+ # as intended.
45
+ #
46
+ def each(&block)
47
+ super { |hash| yield hash.to_a.flatten }
48
+ end
49
+
50
+ # Works like Hash#map.
51
+ def map(&block)
52
+ super { |hash| yield hash.to_a.flatten }
53
+ end
54
+
55
+ # Works like Hash#values.
56
+ def values
57
+ inject([]) { |a, (k, v)| a << v; a }
58
+ end
59
+
60
+ # Returns everything as a hash.
61
+ def to_hash
62
+ inject({}) { |hash, (k, v)| hash[k] = v; hash }
63
+ end
64
+
65
+ def keys
66
+ inject([]) { |a, (k, v)| a << k; a }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ module Helpers
4
+ def css(name, options={})
5
+ show_asset_pack :css, name, options
6
+ end
7
+
8
+ def js(name, options={})
9
+ show_asset_pack :js, name, options
10
+ end
11
+
12
+ def img(src, options={})
13
+ attrs = { :src => src }.merge(options)
14
+
15
+ local = settings.assets.local_file_for src
16
+ if local
17
+ i = Image.new(local)
18
+ attrs[:src] = BusterHelpers.add_cache_buster(src, local)
19
+ if i.dimensions?
20
+ attrs[:width] ||= i.width
21
+ attrs[:height] ||= i.height
22
+ end
23
+ end
24
+
25
+ "<img#{HtmlHelpers.kv attrs} />"
26
+ end
27
+
28
+ def show_asset_pack(type, name, options={})
29
+ pack = settings.assets.packages["#{name}.#{type}"]
30
+ return "" unless pack
31
+
32
+ if settings.production?
33
+ pack.to_production_html options
34
+ else
35
+ pack.to_development_html options
36
+ end
37
+ end
38
+
39
+ def asset_filter_css(str)
40
+ Css.preproc str, settings.assets
41
+ end
42
+
43
+ def asset_path_for(file, from)
44
+ settings.assets.dyn_local_file_for file, from
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,17 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ module HtmlHelpers
4
+ extend self
5
+
6
+ def e(str)
7
+ re = Rack::Utils.escape_html str
8
+ re = re.gsub("&#x2F;", '/') # Rack sometimes insists on munging slashes in Ruby 1.8.
9
+ re
10
+ end
11
+
12
+ def kv(hash)
13
+ hash.map { |k, v| " #{e k}='#{e v}'" }.join('')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ class Image
4
+ def initialize(file)
5
+ @file = file
6
+ end
7
+
8
+ def dimensions
9
+ return @dimensions unless @dimensions.nil?
10
+
11
+ _, _, dim = `identify "#{@file}"`.split(' ')
12
+ w, h = dim.split('x')
13
+
14
+ if w.to_i != 0 && h.to_i != 0
15
+ @dimensions = [w.to_i, h.to_i]
16
+ else
17
+ @dimensions = false
18
+ end
19
+
20
+ rescue => e
21
+ @dimensions = false
22
+ end
23
+
24
+ def dimensions?
25
+ !! dimensions
26
+ end
27
+
28
+ def width
29
+ dimensions? && dimensions[0]
30
+ end
31
+
32
+ def height
33
+ dimensions? && dimensions[1]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,223 @@
1
+ module Sinatra
2
+ module AssetPack
3
+ # Assets.
4
+ #
5
+ # == Common usage
6
+ #
7
+ # SinatraApp.assets {
8
+ # # dsl stuff here
9
+ # }
10
+ #
11
+ # a = SinatraApp.assets
12
+ #
13
+ # Getting options:
14
+ #
15
+ # a.js_compression
16
+ # a.output_path
17
+ #
18
+ # Served:
19
+ #
20
+ # a.served # { '/js' => '/var/www/project/app/js', ... }
21
+ # # (URL path => local path)
22
+ #
23
+ # Packages:
24
+ #
25
+ # a.packages # { 'app.css' => #<Package>, ... }
26
+ # # (name.type => package instance)
27
+ #
28
+ # Build:
29
+ #
30
+ # a.build! { |path| puts "Building #{path}" }
31
+ #
32
+ # Lookup:
33
+ #
34
+ # a.local_path_for('/images/bg.gif')
35
+ # a.served?('/images/bg.gif')
36
+ #
37
+ # a.glob('/js/*.js', '/js/vendor/**/*.js')
38
+ # # Returns a HashArray of (local => remote)
39
+ #
40
+ class Options
41
+ extend Configurator
42
+
43
+ def initialize(app, &blk)
44
+ @app = app
45
+ @js_compression = :jsmin
46
+ @css_compression = :simple
47
+ @output_path = app.public
48
+
49
+ @js_compression_options = Hash.new
50
+ @css_compression_options = Hash.new
51
+
52
+ reset!
53
+
54
+ # Defaults!
55
+ serve '/css', :from => 'app/css'
56
+ serve '/js', :from => 'app/js'
57
+ serve '/images', :from => 'app/images'
58
+
59
+ instance_eval &blk if block_given?
60
+ end
61
+
62
+ # =====================================================================
63
+ # DSL methods
64
+
65
+ def serve(path, options={})
66
+ raise Error unless options[:from]
67
+ return unless File.directory?(File.join(app.root, options[:from]))
68
+
69
+ @served[path] = options[:from]
70
+ end
71
+
72
+ # Undo defaults.
73
+ def reset!
74
+ @served = Hash.new
75
+ @packages = Hash.new
76
+ end
77
+
78
+ # Adds some JS packages.
79
+ #
80
+ # js :foo, '/js', [ '/js/vendor/jquery.*.js' ]
81
+ #
82
+ def js(name, path, files=[])
83
+ @packages["#{name}.js"] = Package.new(self, name, :js, path, files)
84
+ end
85
+
86
+ # Adds some CSS packages.
87
+ #
88
+ # css :app, '/css', [ '/css/screen.css' ]
89
+ #
90
+ def css(name, path, files=[])
91
+ @packages["#{name}.css"] = Package.new(self, name, :css, path, files)
92
+ end
93
+
94
+ attr_reader :app # Sinatra::Base instance
95
+ attr_reader :packages # Hash, keys are "foo.js", values are Packages
96
+ attr_reader :served # Hash, paths to be served.
97
+ # Key is URI path, value is local path
98
+
99
+ attrib :js_compression # Symbol, compression method for JS
100
+ attrib :css_compression # Symbol, compression method for CSS
101
+ attrib :output_path # '/public'
102
+
103
+ attrib :js_compression_options # Hash
104
+ attrib :css_compression_options # Hash
105
+
106
+ # =====================================================================
107
+ # Stuff
108
+
109
+ attr_reader :served
110
+
111
+ def build!(&blk)
112
+ session = Rack::Test::Session.new app
113
+
114
+ packages.each { |_, pack|
115
+ out = session.get(pack.path).body
116
+
117
+ write pack.path, out, &blk
118
+ write pack.production_path, out, &blk
119
+ }
120
+
121
+ files.each { |path, local|
122
+ out = session.get(path).body
123
+ write path, out, &blk
124
+ write BusterHelpers.add_cache_buster(path, local), out, &blk
125
+ }
126
+ end
127
+
128
+ def served?(path)
129
+ !! local_file_for(path)
130
+ end
131
+
132
+ # Returns the local file for a given URI path.
133
+ # Returns nil if a file is not found.
134
+ def local_file_for(path)
135
+ path = path.squeeze('/')
136
+
137
+ uri, local = served.detect { |uri, local| path[0...uri.size] == uri }
138
+
139
+ if local
140
+ path = path[uri.size..-1]
141
+ path = File.join app.root, local, path
142
+
143
+ path if File.exists?(path)
144
+ end
145
+ end
146
+
147
+ # Returns the local file for a given URI path. (for dynamic files)
148
+ # Returns nil if a file is not found.
149
+ # TODO: consolidate with local_file_for
150
+ def dyn_local_file_for(file, from)
151
+ # Remove extension
152
+ file = $1 if file =~ /^(.*)(\.[^\.]+)$/
153
+
154
+ # Remove cache-buster (/js/app.28389.js => /js/app)
155
+ file = $1 if file =~ /^(.*)\.[0-9]+$/
156
+
157
+ Dir[File.join(app.root, from, "#{file}.*")].first
158
+ end
159
+
160
+ # Writes `public/#{path}` based on contents of `output`.
161
+ def write(path, output)
162
+ require 'fileutils'
163
+
164
+ path = File.join(@output_path, path)
165
+ yield path if block_given?
166
+
167
+ FileUtils.mkdir_p File.dirname(path)
168
+ File.open(path, 'w') { |f| f.write output }
169
+ end
170
+
171
+ # Returns the files as a hash.
172
+ def files(match=nil)
173
+ # All
174
+ # A buncha tuples
175
+ tuples = @served.map { |prefix, local_path|
176
+ path = File.expand_path(File.join(@app.root, local_path))
177
+ spec = File.join(path, '**', '*')
178
+
179
+ Dir[spec].map { |f|
180
+ [ to_uri(f, prefix, path), f ] unless File.directory?(f)
181
+ }
182
+ }.flatten.compact
183
+
184
+ Hash[*tuples]
185
+ end
186
+
187
+ # Returns an array of URI paths of those matching given globs.
188
+ def glob(*match)
189
+ tuples = match.map { |spec|
190
+ paths = files.keys.select { |f| File.fnmatch?(spec, f) }.sort
191
+ paths.map { |key| [key, files[key]] }
192
+ }
193
+
194
+ HashArray[*tuples.flatten]
195
+ end
196
+
197
+ def cache
198
+ @cache ||= Hash.new
199
+ end
200
+
201
+ def reset_cache
202
+ @cache = nil && cache
203
+ end
204
+
205
+ private
206
+ # Returns a URI for a given file
207
+ # path = '/projects/x/app/css'
208
+ # to_uri('/projects/x/app/css/file.sass', '/styles', path) => '/styles/file.css'
209
+ #
210
+ def to_uri(f, prefix, path)
211
+ fn = (prefix + f.gsub(path, '')).squeeze('/')
212
+
213
+ # Switch the extension ('x.sass' => 'x.css')
214
+ file_ext = File.extname(fn).to_s[1..-1]
215
+ out_ext = AssetPack.tilt_formats[file_ext]
216
+
217
+ fn = fn.gsub(/\.#{file_ext}$/, ".#{out_ext}") if file_ext && out_ext
218
+
219
+ fn
220
+ end
221
+ end
222
+ end
223
+ end