Update 06/26/2012
I changed the
asset_tag_helper.rb
and
config/initializers/sprockets.rb
file to be more robust and to support a “debug mode”.
Note:
The sowftware I use to generate this blog (Jekyll) and the code syntax highlighter I’m using
don’t support the
#{}
Ruby style string concatenation. In
ALL
the examples below, I’ve changed instances of
"#{string_one}#{string_two}"
to
string_one + string_two
This will have a few side effects, as Ruby automatically calls
#to_s
on objects when using the
#{}
style concatenation, but not the
+
style. If you run into issues, let me know.
At
FutureAdvisor
our main app is still in Rails 2.3, mainly because
upgrading to Rails 3 is kind of a pain. I generally 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 absolutely in love with the Asset Pipeline. So I set out to make it work with our
Rails 2.3 app at FutureAdvisor.
The blog was a bit dated, from December of last year, and it didn’t work quite right. The first steps were spot on though.
Install the gems (hopefully you’re using Bundler):
12345678910111213
# Gemfilegem'sprockets','~> 2.0'# My setup uses SASS, HAML and CoffeeScript, but you can obviously use substitutesgroup:assetsdogem'sass'gem'haml'gem'coffee-script'endgroup:development,:testdogem'sprockets-sass'end
12
# Command line; root of your appbundle 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 initialize so it’s available throughout my app.
# config/initializers/sprockets.rbmoduleSprocketsdefself.env@env||=beginsprockets=Sprockets::Environment.newsprockets.append_path'app/assets/javascripts'sprockets.append_path'app/assets/stylesheets'sprockets.css_compressor=YUI::CssCompressor.newsprockets.js_compressor=YUI::JavaScriptCompressor.newsprocketsendenddefself.manifest@manifest||=Sprockets::Manifest.new(env,Rails.root.join("public","assets","manifest.json"))endend# 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 ProductionRails.configuration.action_view.debug_sprockets=trueunlessRails.env.production?
Now I can refer to
Sprockets.env
wherever I need the
Sprockets::Environment
with the appropriate configuration. 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:
123456789101112131415
# config.ru# we need to protect against multiple includes of the Rails environment (trust me)requireFile.dirname(__FILE__)+'/config/environment'if!defined?(Rails)||!Rails.initialized?require'sprockets'unlessRails.env.production?map'/assets'dosprockets=Sprockets.envrunsprocketsendendmap'/'dorunActionController::Dispatcher.newend
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.
1234
# Command line; root of your appmkdir 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
123
# Command line; root of your appmv 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, and it’ll automagically
interpret your SASS, HAML and CoffeeScript files, but all your helpers
like
javascript_include_tag
and
stylesheet_link_tag
don’t work anymore. Fortunately, we can fix that.
When overriding Rails functionality, I like to put all my overrides in a single
place,
/lib/extensions
so I know where they are. The file tree (for this particular override)
usually looks something like this:
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:
# asset_tag_helper.rb# Overwrite some Asset Tag Helpers to use SprocketsmoduleActionViewmoduleHelpers# Overwrite the javascript_path method to use the 'assets' directory# instead of the default 'javascripts' (Sprockets will figure it out)defjavascript_path(source,options)path=compute_public_path(source,'assets',options.merge(:ext=>'js'))options[:body]?path+"?body=1":pathendalias_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)defstylesheet_path(source,options)path=compute_public_path(source,'assets',options.merge(:ext=>'css'))options[:body]?path+"?body=1":pathendalias_method:path_to_stylesheet,:stylesheet_path# aliased to avoid conflicts with a stylesheet_path named routeend# 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).# defstylesheet_link_tag(*sources)options=sources.extract_options!.stringify_keysdebug=options.key?(:debug)?options.delete(:debug):debug_assets?sources.mapdo|source|ifdebug&&!(digest_available?(source,'css'))&&(asset=asset_for(source,'css'))asset.to_a.map{|dep|stylesheet_tag(dep.logical_path,{:body=>true}.merge(options))}elsesources.map{|source|stylesheet_tag(source,options)}endend.uniq.join("\n").html_safeend# 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).#defjavascript_include_tag(*sources)options=sources.extract_options!.stringify_keysdebug=options.key?(:debug)?options.delete(:debug):debug_assets?sources.mapdo|source|ifdebug&&!(digest_available?(source,'js'))&&(asset=asset_for(source,'js'))asset.to_a.map{|dep|javascript_src_tag(dep.logical_path,{:body=>true}.merge(options))}elsesources.map{|source|javascript_src_tag(source.to_s,options)}endend.uniq.join("\n").html_safeendprivatedefjavascript_src_tag(source,options)body=options.has_key?(:body)?options.delete(:body):falsecontent_tag("script","",{"type"=>Mime::JS,"src"=>path_to_javascript(source,:body=>body)}.merge(options))enddefstylesheet_tag(source,options)body=options.has_key?(:body)?options.delete(:body):falsetag("link",{"rel"=>"stylesheet","type"=>Mime::CSS,"media"=>"screen","href"=>html_escape(path_to_stylesheet(source,:body=>body))}.merge(options),false,false)enddefdebug_assets?Rails.configuration.action_view.debug_sprockets||falseend# 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.defcompute_public_path(source,dir,options={})source=source.to_sreturnsourceifis_uri?(source)source=rewrite_extension(source,options[:ext])ifoptions[: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])sourceenddefrewrite_relative_url_root(source,relative_root_url)relative_root_url&&!(source=~Regexp.new("^"+relative_root_url+"/"))?relative_root_url+source:sourceenddefhas_request?@controller.respond_to?(:request)enddefrewrite_host_and_protocol(source,porotocol=nil)host=compute_asset_host(source)ifhas_request?&&!host.blank?&&!is_uri?(host)host=@controller.request.protocol+hostendhost?host+source:sourceend# Check for a sprockets version of the asset, otherwise use the default rails behaviour.defrewrite_asset_path(source,dir,options={})ifsource[0]==?/sourceelsesource=digest_for(source.to_s)source=Pathname.new("/").join(dir,source).to_ssourceendenddefdigest_available?(logical_path,ext)(manifest=Sprockets.manifest)&&(manifest.assets[logical_path+"."+ext])enddefdigest_for(logical_path)if(manifest=Sprockets.manifest)&&(digest=manifest.assets[logical_path])digestelselogical_pathendenddefrewrite_extension(source,ext)ifext&&File.extname(source)!="."+extsource+"."extelsesourceendenddefis_uri?(path)path=~%r{^[-a-z]+://|^(?:cid|data):|^//}enddefasset_for(source,ext)source=source.to_sreturnnilifis_uri?(source)source=rewrite_extension(source,ext)Sprockets.env[source]rescueSprockets::FileOutsidePathsnilendend
Phew! Now that we’ve monkey patched the Rails
AssetTagHelper
to play nicely with Sprockets, we can do things like this:
# lib/tasks/assets.rakerequire"fileutils"require'pathname'namespace:assetsdodefruby_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"ifRake.application.options.traceif$0=~/rake\.bat\Z/iKernel.exec$0,*argselsefork?ruby(*args):Kernel.exec(FileUtils::RUBY,*args)endend# We are currently running with no explicit bundler group# and/or no explicit environment - we have to reinvoke rake to# execute this task.definvoke_or_reboot_rake_task(task)ifENV['RAILS_GROUPS'].to_s.empty?||ENV['RAILS_ENV'].to_s.empty?ruby_rake_tasktaskelseRake::Task[task].invokeendenddesc"Compile all the assets named in config.assets.precompile"task:precompiledoinvoke_or_reboot_rake_task"assets:precompile:all"endnamespace:precompiledodefinternal_precompile(digest=nil)# Ensure that action view is loaded and the appropriate# sprockets hooks get executed_=ActionView::Basesprockets=Sprockets.envmanifest_path=Pathname.new(Rails.public_path).join("assets","manifest.json")manifest=Sprockets.manifestmanifest.compileendtask:alldoRake::Task["assets:precompile:primary"].invokeendtask:primary=>["assets:environment","tmp:cache:clear"]dointernal_precompileendtask:nondigest=>["assets:environment","tmp:cache:clear"]dointernal_precompile(false)endenddesc"Remove compiled assets"task:cleandoinvoke_or_reboot_rake_task"assets:clean:all"endnamespace:cleandotask:all=>["assets:environment","tmp:cache:clear"]dopublic_asset_path=Pathname.new(Rails.public_path).join("assets")rm_rfpublic_asset_path,:secure=>trueendendtask:environmentdoRake::Task["environment"].invokeendend
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 (because 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
including a manifest file,
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.