dynamic_assets 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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