Jared McFarland

The musings of a 20-something engineer

Sprockets 2 With Rails 2.3

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.

Here’s what I’ll cover:

  1. Installing Sprockets and mounting the Rack app
  2. Overriding Rails AssetTagHelper to be aware of Sprockets specific pathing
  3. 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 last year, and it didn’t work quite right. The first steps were spot on though.

Install the gems (hopefully you’re using Bundler):

1
2
3
4
5
6
7
8
9
10
11
12
13
# Gemfile
gem 'sprockets', '~> 2.0'

# 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
1
2
# 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 initialize so it’s available throughout my app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# config/initializers/sprockets.rb
module Sprockets
  def self.env
    @env ||= begin
      sprockets = Sprockets::Environment.new
      sprockets.append_path 'app/assets/javascripts'
      sprockets.append_path 'app/assets/stylesheets'
      sprockets.css_compressor  = YUI::CssCompressor.new
      sprockets.js_compressor   = YUI::JavaScriptCompressor.new
      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 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# config.ru
# we need to protect against multiple includes of the Rails environment (trust me)
require File.dirname(__FILE__) + '/config/environment' if !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.

1
2
3
4
# 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

1
2
3
# 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, 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:

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:

1
2
# extensions.rb
require 'extensions/action_view'
1
2
# action_view.rb
require 'extensions/action_view/helpers'
1
2
# helpers.rb
require 'extensions/action_view/helpers/asset_tag_helper'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# 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
  end

  # 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 =~ Regexp.new("^" + 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

Phew! Now that we’ve monkey patched the Rails AssetTagHelper to play nicely with Sprockets, we can do things like this:

1
2
3
4
5
6
7
# 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 tacked 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 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
      public_asset_path = Pathname.new(Rails.public_path).join("assets")
      rm_rf public_asset_path, :secure => true
    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 (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.

Comments