dynamic_assets 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 (38) hide show
  1. data/README.rdoc +225 -0
  2. data/app/controllers/assets_controller.rb +4 -0
  3. data/app/helpers/dynamic_assets_helpers.rb +63 -0
  4. data/config/routes.rb +12 -0
  5. data/lib/dynamic_assets/config.rb +109 -0
  6. data/lib/dynamic_assets/controller.rb +40 -0
  7. data/lib/dynamic_assets/core_extensions.rb +29 -0
  8. data/lib/dynamic_assets/engine.rb +11 -0
  9. data/lib/dynamic_assets/manager.rb +10 -0
  10. data/lib/dynamic_assets/reference/javascript_reference.rb +19 -0
  11. data/lib/dynamic_assets/reference/stylesheet_reference.rb +84 -0
  12. data/lib/dynamic_assets/reference.rb +118 -0
  13. data/lib/dynamic_assets.rb +12 -0
  14. data/spec/dummy_rails_app/app/controllers/application_controller.rb +3 -0
  15. data/spec/dummy_rails_app/app/helpers/application_helper.rb +2 -0
  16. data/spec/dummy_rails_app/config/application.rb +42 -0
  17. data/spec/dummy_rails_app/config/boot.rb +6 -0
  18. data/spec/dummy_rails_app/config/environment.rb +5 -0
  19. data/spec/dummy_rails_app/config/environments/development.rb +26 -0
  20. data/spec/dummy_rails_app/config/environments/production.rb +49 -0
  21. data/spec/dummy_rails_app/config/environments/test.rb +35 -0
  22. data/spec/dummy_rails_app/config/initializers/backtrace_silencers.rb +7 -0
  23. data/spec/dummy_rails_app/config/initializers/inflections.rb +10 -0
  24. data/spec/dummy_rails_app/config/initializers/mime_types.rb +5 -0
  25. data/spec/dummy_rails_app/config/initializers/secret_token.rb +7 -0
  26. data/spec/dummy_rails_app/config/initializers/session_store.rb +8 -0
  27. data/spec/dummy_rails_app/config/routes.rb +58 -0
  28. data/spec/dummy_rails_app/db/seeds.rb +7 -0
  29. data/spec/dummy_rails_app/spec/spec_helper.rb +27 -0
  30. data/spec/dummy_rails_app/test/performance/browsing_test.rb +9 -0
  31. data/spec/dummy_rails_app/test/test_helper.rb +13 -0
  32. data/spec/helpers/dynamic_assets_helpers_spec.rb +69 -0
  33. data/spec/lib/dynamic_assets/config_spec.rb +148 -0
  34. data/spec/lib/dynamic_assets/manager_spec.rb +9 -0
  35. data/spec/lib/dynamic_assets/stylesheet_reference_spec.rb +58 -0
  36. data/spec/spec_helper.rb +26 -0
  37. data/spec/support/matchers/string_matchers.rb +61 -0
  38. metadata +219 -0
data/README.rdoc ADDED
@@ -0,0 +1,225 @@
1
+ = dynamic_assets
2
+
3
+ DynamicAssets allows a Rails 3.0 app to serve its JavaScript and CSS assets
4
+ dynamically instead of statically, which makes all kinds of things possible.
5
+ Out of the box it can (optionally):
6
+
7
+ * Combine all CSS files into one for faster downloading.
8
+ * Combine all JavaScript files into one.
9
+ * Minify assets to make them smaller.
10
+ * Run your CSS or JS assets through ERB, like views.
11
+ * Run your CSS assets through a {Sass}[http://sass-lang.com/] pre-processor (sass or scss).
12
+ * Run them through ERB then Sass, what the heck. (Actually, this can be useful, like to allow
13
+ your app to set some Sass variables.)
14
+ * Eliminate the need for deploy-time rake tasks that combine and minify your assets.
15
+ * Combine, minify, and pre-process in memory instead of on disk, to accommodate read-only
16
+ filesystems (e.g. Heroku).
17
+ * Cache the result using Rails action caching, and set the headers for far-future expiration,
18
+ allowing browsers and front-end caches like Varnish to hold assets for a long time.
19
+ * Allow assets to be grouped, much like
20
+ {Scott Becker's venerable asset_packager}[http://synthesis.sbecker.net/pages/asset_packager].
21
+ (Example: You may want a set of stylesheets for your main interface, and another set for your admin
22
+ interface, maybe with some overlap. With DynamicAssets, your normal users won't pay the
23
+ penalty of downloading your admin styles.)
24
+ * Allow CSS assets to refer to static images through relative URLs. That is, it doesn't break CSS urls.
25
+ * Honor Rails' scheme for asset hosts.
26
+ * Honor Rails' scheme for cache-busting.
27
+
28
+ It seems that Rails 3.1 will offer many of these features off-the-shelf, which is
29
+ cool. DynamicAssets allows you to get some of those features today in 3.0, but it
30
+ wasn't intended as a back port or a stopgap. It just happens that serving assets
31
+ dynamically is useful enough that multiple people have thought of implementing it.
32
+ (See also: {Shoebox}[https://github.com/ddollar/shoebox])
33
+
34
+ == How To
35
+
36
+ 1. Add this to your Gemfile:
37
+
38
+ gem "dynamic_assets"
39
+
40
+ 2. Put your CSS files in <tt>app/views/stylesheets</tt> and your JS files in
41
+ <tt>app/views/javascripts</tt>. Each filename's extension triggers an
42
+ optional pre-processor:
43
+
44
+ .css = raw
45
+ .js = raw
46
+ .css.erb = process with ERB
47
+ .js.erb = process with ERB
48
+ .css.sass = process with Sass and assume sass syntax
49
+ .css.scss = process with Sass and assume scss syntax
50
+ .css.sass.erb = process with ERB then Sass (sass)
51
+ .css.scss.erb = process with ERB then Sass (scss)
52
+
53
+ (Note that each file can be processed differently. You could stick one toe
54
+ into the Sass world by renaming one of your .css files to .scss.)
55
+
56
+ 3. Create a config/assets.yml file that looks something like this:
57
+
58
+ ---
59
+
60
+ config:
61
+ production, staging, test:
62
+ combine_asset_groups: true
63
+ minify: true
64
+ cache: true
65
+ development:
66
+ combine_asset_groups: true
67
+ minify: false
68
+ cache: false
69
+
70
+ javascripts:
71
+ - base:
72
+ - foo
73
+ - bar
74
+ - baz
75
+ - third-party/widget
76
+ - admin
77
+ - foo
78
+ - qux
79
+ - quux
80
+
81
+ stylesheets:
82
+ - app:
83
+ - reset
84
+ - application
85
+ - admin
86
+ - reset
87
+ - application
88
+ - admin
89
+
90
+ The assets.yml file sets some config values and then lists your assets. Don't be shy
91
+ about listing your assets; it's a good way to get noticed. This sample config
92
+ file says that in production, foo.js, bar.js, baz.js, and widget.js (which is
93
+ in app/views/javascripts/third-party) should be combined into one file,
94
+ minified, and served by your app as /assets/javascripts/base.js in such a way
95
+ that it'd be cached. In development, those files would be combined but not minified
96
+ or cached.
97
+
98
+ 4. In your layout, replace your usual CSS and JS references with
99
+
100
+ <%= stylesheet_asset_tag :app %>
101
+
102
+ wherever you want your stylesheet tags to appear and
103
+
104
+ <%= javascript_asset_tag :base %>
105
+
106
+ wherever you want your script tags to appear. The symbol argument to each
107
+ helper is the name of the group you defined in assets.yml. The helper will
108
+ produce one tag if the assets are combined, or multiple tags if they're not,
109
+ like the cowboy in <i>Mulholland Dr.</i>
110
+
111
+
112
+ == Variables for ERB
113
+
114
+ By default, assets are served by a small controller whose routes are added to
115
+ your app automatically when the gem is loaded, but you can easily create your
116
+ own controller if you prefer. One reason to do this would be to inject variables
117
+ into an asset via ERB, like this:
118
+
119
+ class AssetsController < ApplicationController
120
+ include DynamicAssets::Controller
121
+
122
+ def show_stylesheet
123
+ @background_color = '#FFE'
124
+ render_asset :stylesheets, params[:name], "text/css"
125
+ end
126
+ end
127
+
128
+ Now in app/views/stylesheets/application.css.erb you could do this:
129
+
130
+ body {
131
+ background-color: <%= @background_color %>;
132
+ }
133
+
134
+
135
+ == Static Images Embedded in CSS
136
+
137
+ Sometimes you'll install a JavaScript plugin that was created by someone
138
+ else, and it will come with a stylesheet and images. The stylesheet may
139
+ reference an image like this:
140
+
141
+ div.thing {
142
+ background: url(fancy_background.png);
143
+ }
144
+
145
+ Browsers will find the image by looking in the same URL path as the
146
+ stylesheet, so in a typical Rails environment, you might add these two
147
+ files to your app as public/stylesheets/thing.css and
148
+ public/stylesheets/fancy_background.png.
149
+
150
+ With DynamicAssets, you'll put them here instead:
151
+
152
+ app/views/stylesheets/thing.css
153
+ public/stylesheets/thing/fancy_background.png
154
+
155
+ and the processor will make sure the embedded URL is turned into a fully
156
+ qualified one that will allow the browser to find
157
+ /stylesheets/thing/fancy_background.png.
158
+
159
+ == CSS Media Types
160
+
161
+ If you have different CSS files for printing than for the screen, create
162
+ separate groups in your assets.yml. Then include both groups in your layout:
163
+
164
+ <%= stylesheet_asset_tag :app %>
165
+ <%= stylesheet_asset_tag :app_printing, :media => "print" %>
166
+
167
+ == Performance
168
+
169
+ In production, assets can typically be cached aggressively. Like Rails,
170
+ dynamic_assets adds a URL parameter to the asset path that will change
171
+ if any of the underlying assets is modified, forcing clients to reload
172
+ the asset. In production, I find dynamic assets to be quite speedy.
173
+
174
+ But dynamic assets can sometimes be annoying during development. The
175
+ sweet spot for my dev environment is to combine assets but not to
176
+ minify or cache them (see assets.yml above). Here's why:
177
+
178
+ === Usually set assets not to be minified in development
179
+
180
+ I usually leave minification off in development, because it does add some
181
+ overhead to each asset request, and it makes them difficult to read if you
182
+ need to debug them (like with Firebug). In production, caching minimizes
183
+ the overhead, but you typically won't cache your assets in development,
184
+ unless you're some sort of nut.
185
+
186
+ === Set assets to be combined, even in development and especially in test
187
+
188
+ By default Rails reloads all classes with each new request in development
189
+ mode. If you're not combining all of your assets, a single page load
190
+ will result in an additional request to your app for each asset, which
191
+ may result in a dozen requests to your dev server for one page, and each
192
+ one will reload all of your classes. Combining assets in dev reduces the
193
+ number of requests, shrinking your page load time. And unlike minification,
194
+ using combined assets in dev is usually not a problem because the concatenated
195
+ files are still quite readable. The one exception is that it makes it more
196
+ difficult to find out which asset file includes the problem you're hunting
197
+ down.
198
+
199
+ <em>Note that one advantage of using DynamicAssets</em> instead of a
200
+ deploy-time task is that you can get more exposure to your processed JavaScript.
201
+ Concatenation and minification can sometimes uncover bugs in your scripts.
202
+ (Example: a forgotten semicolon may be forgiven by a browser if it's at the
203
+ end of a script file but it may cause problems if it's immediately followed by
204
+ more code, tacked on from another file.)
205
+
206
+ Call me silly, but I prefer to find out about that kind of error before I
207
+ deploy my app, and with DynamicAssets I can easily set my test environment to
208
+ combine and minify, so any full-stack testing will expose the problem.
209
+
210
+ === Or, to eliminate the biggest bottleneck, turn off class caching in development
211
+
212
+ If you don't mind to restart your server every time you change a
213
+ bit of Ruby code, you could edit your config/environments/development.rb
214
+ to do this
215
+
216
+ config.cache_classes = true
217
+
218
+ which would eliminate the biggest chunk of overhead in dev, class-reloading
219
+ on every request.
220
+
221
+
222
+ == Copyright
223
+
224
+ Copyright (c) 2011 Robert Davis. See MIT-LICENSE for further details.
225
+
@@ -0,0 +1,4 @@
1
+
2
+ class AssetsController < ActionController::Base
3
+ include DynamicAssets::Controller
4
+ end
@@ -0,0 +1,63 @@
1
+
2
+ module DynamicAssetsHelpers
3
+
4
+ def stylesheet_asset_tag(group_key, http_attributes = {})
5
+ DynamicAssets::Manager.asset_references_for_group_key(:stylesheets, group_key).map do |asset_ref|
6
+ path = stylesheet_asset_path asset_ref.name
7
+ path << "?#{asset_ref.mtime.to_i.to_s}" if asset_ref.mtime.present?
8
+
9
+ tag :link, {
10
+ :type => "text/css",
11
+ :rel => "stylesheet",
12
+ :media => "screen",
13
+ :href => asset_url_for_path(path)
14
+ }.merge!(http_attributes)
15
+
16
+ end.join.html_safe
17
+ end
18
+
19
+ def javascript_asset_tag(group_key, http_attributes = {})
20
+ DynamicAssets::Manager.asset_references_for_group_key(:javascripts, group_key).map do |asset_ref|
21
+ path = javascript_asset_path asset_ref.name
22
+ path << "?#{asset_ref.mtime.to_i.to_s}" if asset_ref.mtime.present?
23
+
24
+ content_tag :script, "", {
25
+ :type => "text/javascript",
26
+ :src => asset_url_for_path(path)
27
+ }.merge!(http_attributes)
28
+
29
+ end.join.html_safe
30
+ end
31
+
32
+
33
+ protected
34
+
35
+ def asset_url_for_path(path)
36
+ raise "expected a path, not a full URL: #{path}" unless path.relative_url?
37
+ path = "/" + path unless path[0,1] == "/"
38
+ host = compute_asset_host path
39
+
40
+ if host
41
+ "#{host}#{path}"
42
+ else
43
+ path
44
+ end
45
+ end
46
+
47
+ # Extracted from Rails' AssetTagHelper, where it's private
48
+ def compute_asset_host(source)
49
+ if host = config.asset_host
50
+ if host.is_a?(Proc) || host.respond_to?(:call)
51
+ case host.is_a?(Proc) ? host.arity : host.method(:call).arity
52
+ when 2
53
+ request = controller.respond_to?(:request) && controller.request
54
+ host.call(source, request)
55
+ else
56
+ host.call(source)
57
+ end
58
+ else
59
+ (host =~ /%d/) ? host % (source.hash % 4) : host
60
+ end
61
+ end
62
+ end
63
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+
2
+ Rails.application.routes.draw do
3
+
4
+ match '/assets/javascripts/:name.js' => 'assets#show_javascript', :as => :javascript_asset,
5
+ :format => "js", # Important for action-caching non-HTML resources
6
+ :constraints => { :name => /[^ ]+/ } # By default, segments can't have dots. We allow all but space.
7
+
8
+ match '/assets/stylesheets/:name.css' => 'assets#show_stylesheet', :as => :stylesheet_asset,
9
+ :format => "css", # Important for action-caching non-HTML resources
10
+ :constraints => { :name => /[^ ]+/ } # By default, segments can't have dots. We allow all but space.
11
+
12
+ end
@@ -0,0 +1,109 @@
1
+ require 'yaml'
2
+
3
+ module DynamicAssets
4
+ module Config
5
+
6
+ CONFIG_VARS = %w(combine_asset_groups minify cache)
7
+
8
+ def init(yml_path)
9
+ @yml_path = yml_path
10
+ assets_hash.present?
11
+ end
12
+
13
+
14
+ #
15
+ # Configuration
16
+ #
17
+
18
+ def combine_asset_groups?
19
+ @combine_asset_groups.nil? ? true : @combine_asset_groups
20
+ end
21
+
22
+ def minify?
23
+ @minify.nil? ? true : @minify
24
+ end
25
+
26
+ def cache?
27
+ @cache.nil? ? true : @cache
28
+ end
29
+
30
+
31
+ #
32
+ # Methods
33
+ #
34
+
35
+ def asset_references_for_group_key(type, group_key)
36
+ assets_hash[type].if_present { |gs| gs[:by_group][group_key] }
37
+ end
38
+
39
+ def asset_reference_for_name(type, name)
40
+ assets_hash[type].if_present { |gs| gs[:by_name][name] }
41
+ end
42
+
43
+
44
+ protected
45
+
46
+ def yml
47
+ return @yml if @yml
48
+
49
+ if File.exists? @yml_path
50
+ @yml = YAML.load_file @yml_path
51
+ @yml.delete("config").if_present { |c| configure c }
52
+ else
53
+ @yml = {}
54
+ end
55
+
56
+ @yml
57
+ end
58
+
59
+ def configure(values)
60
+ values.each do |env_string, config_values|
61
+ next unless env_string.split(/ *, */).include? Rails.env
62
+
63
+ config_values.each do |name, value|
64
+ raise "unknown config variable: #{name}" unless CONFIG_VARS.include? name
65
+ instance_variable_set "@#{name}", value
66
+ end
67
+ end
68
+ end
69
+
70
+ def assets_hash
71
+ return @assets if @assets
72
+
73
+ assets = {}
74
+ yml.each do |key, values|
75
+ next if key == "config" || key.blank?
76
+
77
+ type = key.to_sym
78
+ groups = values
79
+
80
+ typed_assets = assets[type] = {
81
+ :by_name => {},
82
+ :by_group => {}
83
+ }
84
+
85
+ groups.map do |group_hash|
86
+ group_key = group_hash.keys.first
87
+ group_names = group_hash.values.first
88
+
89
+ assets_in_group = if combine_asset_groups?
90
+ # Create a single AssetReference for the group
91
+ name = group_key
92
+ a = typed_assets[:by_name][name] ||=
93
+ Reference.new_for_type(type, :name => name, :member_names => group_names)
94
+ [a]
95
+ else
96
+ # Create an AssetReference for each member of the group
97
+ group_names.map do |name|
98
+ typed_assets[:by_name][name] ||= Reference.new_for_type type, :name => name
99
+ end
100
+ end
101
+
102
+ typed_assets[:by_group][group_key.to_sym] = assets_in_group
103
+ end
104
+ end
105
+
106
+ @assets = assets
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,40 @@
1
+
2
+ module DynamicAssets
3
+ module Controller
4
+
5
+ def self.included(base)
6
+ base.caches_action :show_stylesheet, :show_javascript, :expires_in => 60.minutes,
7
+ :if => Proc.new { Manager.cache? }
8
+ end
9
+
10
+
11
+ #
12
+ # Actions
13
+ #
14
+
15
+ def show_stylesheet
16
+ render_asset :stylesheets, params[:name], "text/css"
17
+ end
18
+
19
+ def show_javascript
20
+ render_asset :javascripts, params[:name], "text/javascript"
21
+ end
22
+
23
+
24
+ protected
25
+
26
+ def render_asset(type, name, mime_type)
27
+ asset = Manager.asset_reference_for_name type, name
28
+ raise ActionController::RoutingError.new "No route matches \"#{request.path}\"" unless asset
29
+
30
+ if Manager.cache?
31
+ response.cache_control[:public] = true
32
+ response.cache_control[:max_age] = 365.days
33
+ headers["Expires"] = (Time.now + 365.days).utc.httpdate
34
+ end
35
+
36
+ render :layout => false, :text => asset.content(binding), :content_type => mime_type
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,29 @@
1
+
2
+ class String
3
+
4
+ # Returns true iff the receiver seems to be a relative, not a full, URL.
5
+ # By "relative url" we mean a URL with no host info, although it may
6
+ # begin with a slash.
7
+ def relative_url?
8
+ regexp = /^[^:\/]*:\/\/[^\/]*/
9
+ self[regexp].nil?
10
+ end
11
+
12
+ end
13
+
14
+ class Object
15
+
16
+ def if_present(*value)
17
+ raise ArgumentError, "Specify either a value or a block but not both" if value.empty? != block_given?
18
+ raise ArgumentError, "Too many arguments. Expected one." if value.length > 1
19
+
20
+ if !self.present?
21
+ self
22
+ elsif block_given?
23
+ yield self
24
+ else
25
+ value.first
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module DynamicAssets
3
+ class Engine < Rails::Engine
4
+
5
+ initializer 'dynamic_assets.config' do |app|
6
+ Manager.init "#{app.root}/config/assets.yml"
7
+ ActionView::Base.send :include, DynamicAssetsHelpers
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ require 'yaml'
2
+
3
+ module DynamicAssets
4
+
5
+ module Manager
6
+ extend Config
7
+ extend self # act like a singleton class
8
+ end
9
+
10
+ end
@@ -0,0 +1,19 @@
1
+ module DynamicAssets
2
+
3
+ class JavascriptReference < Reference
4
+
5
+ def formats
6
+ %w(js)
7
+ end
8
+
9
+ def type
10
+ :javascripts
11
+ end
12
+
13
+ def minify(content_string)
14
+ JSMin.minify content_string
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,84 @@
1
+
2
+ module DynamicAssets
3
+
4
+ class StylesheetReference < Reference
5
+
6
+ # CSS style sheets can contain relative urls like this:
7
+ #
8
+ # background: url(something.png)
9
+ #
10
+ # The browser will look for the resource in the same location as
11
+ # the CSS file. However, we serve static resources like images
12
+ # from a different location, so the StylesheetReference will prepend
13
+ # RELATIVE_URL_ROOT to each such relative url in a stylesheet.
14
+
15
+ RELATIVE_URL_ROOT = "/stylesheets"
16
+
17
+
18
+ def formats
19
+ %w(sass scss css)
20
+ end
21
+
22
+ def type
23
+ :stylesheets
24
+ end
25
+
26
+ def minify(content_string)
27
+ # From asset_packager. PENDING: replace with github.com/rgrove/cssmin ?
28
+ content_string.gsub(/\s+/, " ").# collapse space
29
+ gsub(/\/\*(.*?)\*\//, ""). # remove comments
30
+ gsub(/\} /, "}\n"). # add line breaks
31
+ gsub(/\n$/, ""). # remove last break
32
+ gsub(/ \{ /, " {"). # trim inside brackets
33
+ gsub(/; \}/, "}") # trim inside brackets
34
+ end
35
+
36
+
37
+ protected
38
+
39
+ # Overridden to transform URLs embedded in the CSS
40
+ def read_member(member_name)
41
+ content_string = super
42
+ format = format_for_member_name member_name
43
+
44
+ content_string = case format
45
+ when :css
46
+ content_string
47
+ when :sass,:scss
48
+ location = File.dirname path_for_member_name(member_name)
49
+ Sass::Engine.new(content_string, :syntax => format, :load_paths => [location],
50
+ :cache => false).render
51
+ else raise "unknown format #{format}"
52
+ end
53
+
54
+ # PENDING: we could do something similar to insert the asset host,
55
+ # although we'd need to pass some context (namely the request) down
56
+ # from the controller to compute the asset host in the same way Rails
57
+ # does.
58
+ transform_urls member_name, content_string
59
+ end
60
+
61
+
62
+ def transform_urls(member_name, content_string)
63
+
64
+ # Prepend url_root to each relative url that doesn't start with a slash.
65
+ #
66
+ # Inside fancy.css, any of these:
67
+ # url(one/thing.png)
68
+ # url('one/thing.png')
69
+ # url( "one/thing.png" )
70
+ # url(../one/thing.png)
71
+ # will become
72
+ # url(/stylesheets/fancy/one/thing.png)
73
+ #
74
+
75
+ content_string.gsub( /url\( *['"]?([^)]*[^'"])['"]? *\)/ ) do |s|
76
+ url = $1
77
+ url.gsub! /^(\.\.|\.)\//, ''
78
+ (url !~ /^\// && url.relative_url?) ? "url(#{RELATIVE_URL_ROOT}/#{member_name}/#{url})" : s
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ end