Your assets are covered.
With Rails’ default asset caching, CSS and JS are concatenated (not even minified) at runtime when that bundle is first requested. Not good enough. AssetHat can automatically:
- Easily minify and bundle CSS and JS on deploy to reduce file sizes and HTTP requests.
- Load popular third-party JS (like jQuery and Prototype) from Google's CDN when in production, or from localhost in development.
- Force image URLs in your CSS to use CDN subdomains, not just the current host.
- Add an image’s last Git commit ID to its CSS URLs to bust browser caches (e.g., /images/foo.png?ab12cd3).
After setup, you can use these in your layouts and views:
<%= include_css :bundle => 'application' %> # => <link href="/stylesheets/bundles/application.min.css" # media="screen,projection" rel="stylesheet" type="text/css" /> <%= include_js :bundles => ['plugins', 'common'] %> # => <script src="/javascripts/bundles/plugins.min.js" # type="text/javascript"></script> # <script src="/javascripts/bundles/common.min.js" # type="text/javascript"></script>
And this in your deploy script:
rake asset_hat:minify
See README.rdoc for more.
Methods
public class
Constants
VERSION | = | self.version | This gem’s version number. | |
TYPES | = | [:css, :js] | Types of supported assets; currently [:css, :js]. | |
ASSETS_DIR | = | defined?(Rails.public_path) ? Rails.public_path : 'public' | Base directory in which all assets are kept, e.g., ‘public/’. | |
STYLESHEETS_DIR | = | File.join(ASSETS_DIR, 'stylesheets') | Directory in which all stylesheets are kept, e.g., ‘public/stylesheets/’. | |
JAVASCRIPTS_DIR | = | File.join(ASSETS_DIR, 'javascripts') | Directory in which all JavaScripts are kept, e.g., ‘public/javascripts/’. | |
RELATIVE_CONFIG_FILEPATH | = | File.join('config', 'assets.yml') | Relative path for the config file. | |
CONFIG_FILEPATH | = | File.join(RAILS_ROOT, RELATIVE_CONFIG_FILEPATH) | Absolute path for the config file. |
Public class methods
Returns true if the specified asset exists in the file system:
AssetHat.asset_exists?('application.css', :css) # => true if public/stylesheets/application.css exists AssetHat.asset_exists?('some-plugin.js', :js) # => true if public/javascripts/some-plugin.js exists
See also AssetHat.assets_dir.
# File lib/asset_hat.rb, line 127 127: def self.asset_exists?(filename, type) 128: # Process arguments 129: type = type.to_sym 130: unless TYPES.include?(type) 131: raise "Unknown type \"#{type}\"; should be one of: #{TYPES.join(', ')}." 132: return 133: end 134: 135: # Default to `{:css => {}, :js => {}}` 136: @asset_exists ||= TYPES.inject({}) { |hsh, t| hsh.merge(t => {}) } 137: 138: if @asset_exists[type][filename].nil? 139: @asset_exists[type][filename] = 140: File.exist?(File.join(self.assets_dir(type), filename)) 141: end 142: @asset_exists[type][filename] 143: end
Returns the relative path to the directory where the original CSS or JS files are stored.
type argument: :css or :js
# File lib/asset_hat.rb, line 76 76: def self.assets_dir(type) 77: type = type.to_sym 78: 79: unless TYPES.include?(type) 80: raise "Unknown type \"#{type}\"; should be one of: #{TYPES.join(', ')}." 81: return 82: end 83: 84: case type 85: when :css ; STYLESHEETS_DIR 86: when :js ; JAVASCRIPTS_DIR 87: else nil 88: end 89: end
Returns the extension-less names of files in the given bundle:
AssetHat.bundle_filenames('application', :css) # => ['reset', 'application', 'clearfix'] AssetHat.bundle_filenames('non-existent-file', :css) # => nil
# File lib/asset_hat.rb, line 169 169: def self.bundle_filenames(bundle, type) 170: # Process arguments 171: unless TYPES.include?(type.to_sym) 172: raise "Unknown type \"#{type}\"; should be one of: #{TYPES.join(', ')}." 173: return 174: end 175: 176: self.config[type.to_s]['bundles'][bundle.to_s] rescue nil 177: end
Returns the full paths of files in the given bundle:
AssetHat.bundle_filenames('application', :css) # => ['/path/to/app/public/stylesheets/reset.css', '/path/to/app/public/stylesheets/application.css', '/path/to/app/public/stylesheets/clearfix.css'] AssetHat.bundle_filenames('non-existent-file', :css) # => nil
# File lib/asset_hat.rb, line 187 187: def self.bundle_filepaths(bundle, type) 188: # Process arguments 189: unless TYPES.include?(type.to_sym) 190: raise "Unknown type \"#{type}\"; should be one of: #{TYPES.join(', ')}." 191: return 192: end 193: 194: dir = self.assets_dir(type) 195: filenames = self.bundle_filenames(bundle, type) 196: filepaths = filenames.present? ? 197: filenames.map { |fn| File.join(dir, "#{fn}.#{type}") } : nil 198: end
Returns the relative path to the directory where CSS or JS bundles are stored.
Usage:
AssetHat.bundles_dir # => 'bundles' AssetHat.bundles_dir(:ssl => true) # => 'bundles/ssl' AssetHat.bundles_dir(:css) # => 'public/stylesheets/bundles' AssetHat.bundles_dir(:js, :ssl => true) # => 'public/javascripts/bundles/ssl
Options:
- ssl
- Set this to true if the stylesheet references images via SSL. Defaults to false.
# File lib/asset_hat.rb, line 109 109: def self.bundles_dir(*args) 110: options = args.extract_options! 111: options.symbolize_keys!.reverse_merge!(:ssl => false) 112: type = args.first 113: 114: dir = type.present? ? File.join(assets_dir(type), 'bundles') : 'bundles' 115: dir = File.join(dir, 'ssl') if options[:ssl] 116: dir 117: end
Returns true if bundles should be included as single minified files (e.g., in production), or false if bundles should be included as separate, unminified files (e.g., in development). To modify this value, set config.action_controller.perform_caching = true in your environment.
# File lib/asset_hat.rb, line 150 150: def self.cache? ; ActionController::Base.perform_caching ; end
Precomputes and caches the last commit ID for all bundles. Your web server process(es) should run this at boot to avoid overhead during user runtime.
# File lib/asset_hat/vcs.rb, line 74 74: def self.cache_last_commit_ids 75: AssetHat::TYPES.each do |type| 76: next if AssetHat.config[type.to_s].blank? || 77: AssetHat.config[type.to_s]['bundles'].blank? 78: 79: AssetHat.config[type.to_s]['bundles'].keys.each do |bundle| 80: # Memoize commit ID for this bundle 81: AssetHat.last_bundle_commit_id(bundle, type) if AssetHat.cache? 82: 83: # Memoize commit IDs for each file in this bundle 84: AssetHat.bundle_filepaths(bundle, type).each do |filepath| 85: AssetHat.last_commit_id(filepath) 86: end 87: end 88: end 89: end
# File lib/asset_hat.rb, line 249 249: def self.clear_html_cache 250: html_cache = {} 251: end
Reads ActionController::Base.asset_host, which can be a String or Proc, and returns a String. Should behave just like Rails 2.3.x’s private `compute_asset_host` method, but with a simulated request.
Example environment config for CDN support via SSL:
# In config/environments/production.rb: config.action_controller.asset_host = Proc.new do |source, request| "#{request.protocol}cdn#{source.hash % 4}.example.com" # => 'http://cdn0.example.com', 'https://cdn1.example.com', etc. end
If your CDN doesn’t have SSL support, you can instead revert SSL pages to serving assets from your web server:
config.action_controller.asset_host = Proc.new do |source, request| request.ssl? ? nil : "http://cdn#{source.hash % 4}.example.com" end
Options:
- ssl
- Set to true to simulate a request via SSL. Defaults to false.
# File lib/asset_hat.rb, line 223 223: def self.compute_asset_host(asset_host, source, options={}) 224: host = asset_host 225: if host.is_a?(Proc) || host.respond_to?(:call) 226: case host.is_a?(Proc) ? 227: host.arity : host.method(:call).arity 228: when 2 229: request = ActionController::Request.new( 230: 'HTTPS' => options[:ssl] ? 'on' : 'off') 231: host = host.call(source, request) 232: else 233: host = host.call(source) 234: end 235: else 236: host %= (source.hash % 4) if host =~ /%d/ 237: end 238: host 239: end
Nested-hash version of config/assets.yml.
# File lib/asset_hat.rb, line 65 65: def self.config 66: if !cache? || @config.blank? 67: @config = YAML.load(ERB.new(File.read(CONFIG_FILEPATH)).result) 68: end 69: @config 70: end
Usage:
AssetHat.last_bundle_commit_id('application', :css)
Returns a string of the latest commit ID for the given bundle, based on which of its files were most recently modified in the repository. If no ID can be found, `nil` is returned.
# File lib/asset_hat/vcs.rb, line 45 45: def self.last_bundle_commit_id(bundle, type) 46: # Process arguments 47: type = type.to_sym 48: unless TYPES.include?(type) 49: raise "Unknown type \"#{type}\"; should be one of: #{TYPES.join(', ')}." 50: return 51: end 52: 53: # Default to `{:css => {}, :js => {}}` 54: @last_bundle_commit_ids ||= 55: TYPES.inject({}) { |hsh, t| hsh.merge(t => {}) } 56: 57: if @last_bundle_commit_ids[type][bundle].blank? 58: dir = self.assets_dir(type) 59: filepaths = self.bundle_filepaths(bundle, type) 60: if filepaths.present? 61: @last_bundle_commit_ids[type][bundle] = self.last_commit_id(*filepaths) 62: end 63: end 64: 65: @last_bundle_commit_ids[type][bundle] 66: end
Usage:
AssetHat.last_commit_id('public/stylesheets/application.css') AssetHat.last_commit_id('public/stylesheets/ie.css', 'public/stylesheets/ie7.css', 'public/stylesheets/ie6.css')
Returns a string of the commit ID for the file with the most recent commit. If the file(s) cannot be found, `nil` is returned. Options:
- vcs
- Version control system. Currently, the only supported value is :git.
# File lib/asset_hat/vcs.rb, line 18 18: def self.last_commit_id(*args) 19: # Process arguments 20: options = args.extract_options! 21: options = options.symbolize_keys.reverse_merge(:vcs => :git) 22: filepaths = args.join(' ') 23: 24: # Validate options 25: if options[:vcs] != :git 26: raise 'Git is currently the only supported VCS.' and return 27: end 28: 29: @last_commit_ids ||= {} 30: if @last_commit_ids[filepaths].blank? 31: h = `git log -1 --pretty=format:%h #{filepaths} 2>/dev/null` 32: # `h` has either the abbreviated Git commit hash or an empty string 33: @last_commit_ids[filepaths] = h if h.present? 34: end 35: @last_commit_ids[filepaths] 36: end
Returns the expected path for the minified version of an asset:
AssetHat.min_filepath('public/stylesheets/bundles/application.css', 'css') # => 'public/stylesheets/bundles/application.min.css'
See also AssetHat::CSS.min_filepath and AssetHat::JS.min_filepath.
# File lib/asset_hat.rb, line 159 159: def self.min_filepath(filepath, extension) 160: filepath.sub(/([^\.]*).#{extension}$/, "\\1.min.#{extension}") 161: end
Returns true if the asset host differs between SSL and non-SSL pages, or false if the asset host doesn’t change.
# File lib/asset_hat.rb, line 243 243: def self.ssl_asset_host_differs? 244: asset_host = ActionController::Base.asset_host 245: AssetHat.compute_asset_host(asset_host, 'x.png') != 246: AssetHat.compute_asset_host(asset_host, 'x.png', :ssl => true) 247: end
Returns this gem’s version number. See also VERSION.
# File lib/asset_hat/version.rb, line 3 3: def self.version 4: data_filepath = File.join(File.dirname(__FILE__), %w[.. .. VERSION.yml]) 5: data = YAML.load(File.open(data_filepath, 'r')) 6: [:major, :minor, :patch, :build]. 7: map { |x| data[x] }.reject(&:blank?).join('.') 8: end