Sprockets with Rails 2.3
At FutureAdvisor our main app is still in Rails 2.3 because upgrading to Rails 3 is kind of a pain. I like to keep a few side projects around, like MyRoommate, to play around with new technologies. I’ve upgraded MyRoommate to use Rails 3.2 and am in love with the Asset Pipeline. So I set out to make it work with our Rails 2.3 app at FutureAdvisor.
Here’s what I’ll cover:
- Installing Sprockets and mounting the Rack app
- Overriding Rails AssetTagHelper to be aware of Sprockets specific pathing
- Precompiling assets for use in production (and Sprockets manifest files).
Obviously, the first step was Google. I came across this question on StackOverflow which mentioned this blog from PivotalLabs.
The blog was a bit dated, from December of 2011, and it didn’t work quite right. The first steps were spot on though.
Install the gems (hopefully you’re using Bundler):
# Gemfile
gem 'sprockets', '~> 2.11'
# My setup uses SASS, HAML and CoffeeScript, but you can obviously use substitutes
group :assets do
gem 'sass'
gem 'haml'
gem 'coffee-script'
end
group :development, :test do
gem 'sprockets-sass'
end
# Command line; root of your app
bundle install
This is where my instructions diverge from those of the fine folks at PivotalLabs. You’ll have to create a Sprockets::Environment
for use in several places. The first place is with Rack. Sprockets creates a Rack middleware app that will respond to requests at /assets
with the proper compiled file. I like to store the Sprockets::Environment
in an initializer so it’s available throughout my app.
# This configures some global settins for Sprockets
# that both the config.ru (Rackup file) and Rails will need
#
module Sprockets
def self.env
@env ||= begin
sprockets = Sprockets::Environment.new
sprockets.cache = Sprockets::Cache::FileStore.new(File.join(Rails.root, "tmp", "cache", "assets"))
sprockets.append_path 'app/assets/javascripts'
sprockets.append_path 'app/assets/stylesheets'
sprockets.css_compressor = :scss unless Rails.env.development?
sprockets.js_compressor = :uglify unless Rails.env.development?
sprockets
end
end
def self.manifest
@manifest ||= Sprockets::Manifest.new(env, Rails.root.join("public", "assets", "manifest.json"))
end
end
# This controls whether or not to use "debug" mode with sprockets. In debug mode,
# asset tag helpers (like javascript_include_tag) will output the non-compiled
# version of assets, instead of the compiled version. For example, if you had
# application.js as follows:
#
# = require jquery
# = require event_bindings
#
# javascript_include_tag "application" would output:
# <script src="/assets/jquery.js?body=1"></script>
# <script src="/assets/event_bindings.js?body=1"></script>
#
# If debug mode were turned off, you'd just get
# <script src="/assets/application.js"></script>
#
# Here we turn it on for all environments but Production
Rails.configuration.action_view.debug_sprockets = true unless Rails.env.production?
Now I can use Sprockets.env
wherever I need the Sprockets::Environment
. We’ll go into more detail about the Sprockets.manifest
method in a little bit.
In your Rack middleware file (config.ru
if you don’t have it, just make it and place it in the root directory of your app), add the following:
# we need to protect against multiple includes of the Rails environment (trust me)
require File.dirname(__FILE__) + '/config/environment' unless defined?(Rails) && Rails.initialized?
require 'sprockets'
unless Rails.env.production?
map '/assets' do
sprockets = Sprockets.env
run sprockets
end
end
map '/' do
run ActionController::Dispatcher.new
end
Notice how we only mount this app if we’re not in production? This is because we want to avoid compiling assets on the fly in our production environment. We’d rather precompile all assets, and then serve them statically. More on that later. First, let’s setup your asset directories.
Sprockets is going to look for your assets in the /app/assets
directory, so we need to create those now.
# Command line; root of your app
mkdir app/assets
mkdir app/assets/javascripts
mkdir app/assets/stylesheets
Now you need to move all your assets from your /public
directory to your /app/assets
directories so Sprockets can find them.
# Command line; root of your app
mv public/stylesheets/* app/assets/stylesheets/
mv public/javascripts/* app/assets/javascripts/
If you have a stylesheet named application.css
you should now be able to browse to http://localhost:3000/assets/application.css and see your stylesheet.
At this point, you’ve got Sprockets mounted. It’ll automagically interpret your SASS, HAML and CoffeeScript files. Unfortunately, all your helpers like javascript_include_tag
and stylesheet_link_tag
don’t work anymore. We can fix that.
When overriding Rails functionality, I like to put all my overrides in a single place: /lib/extensions
. The file tree (for this particular override) usually looks something like this:
lib
|
- extenstions/
|
- action_view/
|
- helpers/
|
- asset_tag_helper.rb
- helpers.rb
- action_view.rb
- extensions.rb
It might be a little bit of overkill just for this one override, but it helps in the long run if you plan on adding more overrides or extensions in the future. Here’s the contents of each file:
# extensions.rb
require 'extensions/action_view'
# action_view.rb
require 'extensions/action_view/helpers'
# helpers.rb
require 'extensions/action_view/helpers/asset_tag_helper'
# extensions/action_view/helpers/asset_tag_helper.rb
# Overwrite some Asset Tag Helpers to use Sprockets
module ActionView
module Helpers
# Overwrite the javascript_path method to use the 'assets' directory
# instead of the default 'javascripts' (Sprockets will figure it out)
def javascript_path(source, options)
path = compute_public_path(source, 'assets', options.merge(:ext => 'js'))
options[:body] ? "#{path}?body=1" : path
end
alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
# Overwrite the stylesheet_path method to use the 'assets' directory
# instead of the default 'stylesheets' (Sprockets will figure it out)
def stylesheet_path(source, options)
path = compute_public_path(source, 'assets', options.merge(:ext => 'css'))
options[:body] ? "#{path}?body=1" : path
end
alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
# Overwrite the stylesheet_link_tag method to expand sprockets files if
# debug mode is turned on. Never cache files (like the default Rails 2.3 does).
#
def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys
debug = options.key?(:debug) ? options.delete(:debug) : debug_assets?
sources.map do |source|
if debug && !(digest_available?(source, 'css')) && (asset = asset_for(source, 'css'))
asset.to_a.map { |dep| stylesheet_tag(dep.logical_path, { :body => true }.merge(options)) }
else
sources.map { |source| stylesheet_tag(source, options) }
end
end.uniq.join("\n").html_safe
end
# Overwrite the javascript_include_tag method to expand sprockets files if
# debug mode is turned on. Never cache files (like the default Rails 2.3 does).
#
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
debug = options.key?(:debug) ? options.delete(:debug) : debug_assets?
sources.map do |source|
if debug && !(digest_available?(source, 'js')) && (asset = asset_for(source, 'js'))
asset.to_a.map { |dep| javascript_src_tag(dep.logical_path, { :body => true }.merge(options)) }
else
sources.map { |source| javascript_src_tag(source.to_s, options) }
end
end.uniq.join("\n").html_safe
end
private
def javascript_src_tag(source, options)
body = options.has_key?(:body) ? options.delete(:body) : false
content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source, :body => body) }.merge(options))
end
def stylesheet_tag(source, options)
body = options.has_key?(:body) ? options.delete(:body) : false
tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source, :body => body)) }.merge(options), false, false)
end
def debug_assets?
Rails.configuration.action_view.debug_sprockets || false
end
# Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
# Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
# roots. Rewrite the asset path for cache-busting asset ids. Include
# asset host, if configured, with the correct request protocol.
def compute_public_path(source, dir, options = {})
source = source.to_s
return source if is_uri?(source)
source = rewrite_extension(source, options[:ext]) if options[:ext]
source = rewrite_asset_path(source, dir, options)
source = rewrite_relative_url_root(source, ActionController::Base.relative_url_root)
source = rewrite_host_and_protocol(source, options[:protocol])
source
end
def rewrite_relative_url_root(source, relative_root_url)
relative_root_url && !(source =~ %r{^#{relative_root_url}/}) ? "#{relative_root_url}#{source}" : source
end
def has_request?
@controller.respond_to?(:request)
end
def rewrite_host_and_protocol(source, porotocol = nil)
host = compute_asset_host(source)
if has_request? && !host.blank? && !is_uri?(host)
host = "#{@controller.request.protocol}#{host}"
end
host ? "#{host}#{source}" : source
end
# Check for a sprockets version of the asset, otherwise use the default rails behaviour.
def rewrite_asset_path(source, dir, options = {})
if source[0] == ?/
source
else
source = digest_for(source.to_s)
source = Pathname.new("/").join(dir, source).to_s
source
end
end
def digest_available?(logical_path, ext)
(manifest = Sprockets.manifest) && (manifest.assets["#{logical_path}.#{ext}"])
end
def digest_for(logical_path)
if (manifest = Sprockets.manifest) && (digest = manifest.assets[logical_path])
digest
else
logical_path
end
end
def rewrite_extension(source, ext)
if ext && File.extname(source) != ".#{ext}"
"#{source}.#{ext}"
else
source
end
end
def is_uri?(path)
path =~ %r{^[-a-z]+://|^(?:cid|data):|^//}
end
def asset_for(source, ext)
source = source.to_s
return nil if is_uri?(source)
source = rewrite_extension(source, ext)
Sprockets.env[source]
rescue Sprockets::FileOutsidePaths
nil
end
end
end
Phew! Now that we’ve monkey patched the Rails AssetTagHelper to play nicely with Sprockets, we can do things like this:
# ERB
<%= javascript_include_tag 'application' %>
# HAML
= javascript_include_tag 'application'
# => <link href='/assets/application.css' media='screen' rel='stylesheet' type='text/css' />
At this point, we’ve tackled two of the three main objectives; installing Sprockets and the asset helpers. Now we need to talk about precompiling.
We’ve actually done most of the setup for precompiling already. I took a look at the Rails assets.rake
file and copied most of it to create my own:
# lib/tasks/assets.rake
require "fileutils"
require 'pathname'
namespace :assets do
def ruby_rake_task(task, fork = true)
env = ENV['RAILS_ENV'] || 'production'
groups = ENV['RAILS_GROUPS'] || 'assets'
args = [$0, task,"RAILS_ENV=#{env}","RAILS_GROUPS=#{groups}"]
args << "--trace" if Rake.application.options.trace
if $0 =~ /rake\.bat\Z/i
Kernel.exec $0, *args
else
fork ? ruby(*args) : Kernel.exec(FileUtils::RUBY, *args)
end
end
# We are currently running with no explicit bundler group
# and/or no explicit environment - we have to reinvoke rake to
# execute this task.
def invoke_or_reboot_rake_task(task)
if ENV['RAILS_GROUPS'].to_s.empty? || ENV['RAILS_ENV'].to_s.empty?
ruby_rake_task task
else
Rake::Task[task].invoke
end
end
desc "Compile all the assets named in config.assets.precompile"
task :precompile do
invoke_or_reboot_rake_task "assets:precompile:all"
end
namespace :precompile do
def internal_precompile(digest = nil)
# Ensure that action view is loaded and the appropriate
# sprockets hooks get executed
_ = ActionView::Base
sprockets = Sprockets.env
manifest_path = Pathname.new(Rails.public_path).join("assets", "manifest.json")
manifest = Sprockets.manifest
manifest.compile
end
task :all do
Rake::Task["assets:precompile:primary"].invoke
end
task :primary => ["assets:environment", "tmp:cache:clear"] do
internal_precompile
end
task :nondigest => ["assets:environment", "tmp:cache:clear"] do
internal_precompile(false)
end
end
desc "Remove compiled assets"
task :clean do
invoke_or_reboot_rake_task "assets:clean:all"
end
namespace :clean do
task :all => ["assets:environment", "tmp:cache:clear"] do
Sprockets.manifest.clobber
end
end
task :environment do
Rake::Task["environment"].invoke
end
end
That’s a lot of code to give use two rake tasks; rake assets:precompile
and rake assets:clean
. Keep in mind both of those tasks will run in ‘production’ by default (that’s generally the only place you want compiled assets).
The first task, rake assets:precompile
will generate all of your compiled assets in /public/assets
. This includes manifest.json
which contains the mapping from uncompiled asset to precompiled asset.
The second task, rake assets:clean
will delete all of your precompiled assets.
That’s it! You should now be ready to use Sprockets with Rails 2.3.