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:

  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 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.

 
8
Kudos
 
8
Kudos

Now read this

Creating a PHP extension in Rust

UPDATE: A few hours after posting the initial draft of this I realized my PHP benchmark was broken. I’ve since updated the PHP and Rust versions to be more fair. You can see the changes in the GitHub repo (link at the bottom). Last... Continue →