# Asset helpers for Sinatra applications. Adds asset timestamping with caching, # and javascript/stylesheet bundling. To use, include in app file like so: # # helpers AssetHelpers # # Code is mostly lifted from ActionPack, with slight modifications. module AssetHelpers @@asset_timestamps_cache = {} # Add a file-updated timestamp to an asset in the public directory, # e.g. image, stylesheet, javascript, flash etc. Timestamps are cached in a # class variable, so you only have to check the filesystem once. Example: # # asset_path('images/icon.png') # => "images/icon.png?1255799678" def asset_path(source) "#{source}?#{asset_timestamp(source)}" end # Bundle included javascript files into one concatenated file, to reduce # HTTP lookups. Bundling only occurs when the bundle_assets setting is true. Example: # # In app: # set :bundle_assets, production? # # In views: # javascript_include_tag( # 'javascripts/jquery-1.3.2.js', # 'javascripts/application.js', # :cache => 'javascripts/all.js' # ) # # Asset timestamp will be appended to js include tag src url. def javascript_include_tag(*args) bundled_asset_include_tag(*args) do |cache| %{} end end # Bundle included stylesheets into one concatenated file, to reduce # # HTTP lookups. Bundling only occurs when the bundle_assets setting is true. Example: # # In app: # set :bundle_assets, production? # # In views: # stylesheet_link_tag( # 'stylesheets/base.css', # 'stylesheets/application.css', # :cache => 'stylesheets/all.css' # ) # # Asset timestamp will be appended to stylesheet link tag href url. def stylesheet_link_tag(*args) bundled_asset_include_tag(*args) do |cache| %{} end end private # Modified from Rails' asset timestamping # Cache writes aren't threadsafe, but, using Passenger, we're not deploying # to a mutithreaded environment, so not an issue. Could add a mutex (as Rails does) # if thread safety becomes an issue def asset_timestamp(source) if timestamp = @@asset_timestamps_cache[source] timestamp else path = asset_file_path(source) timestamp = File.exist?(path) ? File.mtime(path).to_i.to_s : '' @@asset_timestamps_cache[source] = timestamp end end def bundled_asset_include_tag(*args) opts = args.last.is_a?(Hash) ? args.pop : {} cache = opts.delete(:cache) if cache && settings.respond_to?(:bundle_assets) && settings.bundle_assets file_path = asset_file_path(cache) unless File.exist?(file_path) write_asset_file_contents(file_path, args) end yield cache else args.map {|f| yield f}.join("\n") end end def join_asset_file_contents(paths) paths.collect { |path| File.read(asset_file_path(path)) }.join("\n\n") end def write_asset_file_contents(joined_asset_path, asset_paths) FileUtils.mkdir_p(File.dirname(joined_asset_path)) File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) } # Set mtime to the latest of the combined files to allow for # consistent ETag without a shared filesystem. mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max File.utime(mt, mt, joined_asset_path) end def asset_file_path(path) File.join('public', path) end end