Thursday, April 12, 2012

Saving your Ass-et Pipeline

Getting your Ass-ets in Gear

The Rails 3.1+ Asset Pipeline is a thing of beauty. Not only does it greatly improve the ability to cache forever these often expensive assets (javascript, css, images), but it compresses and delivers them far more efficiently.

It can also be a pain in the ass-et to ensure that the world does not come crashing down when you push your newly upgraded Rails app to production.

One of the great distinctions between previous versions of Rails and 3.1 is the high likelihood that though your development environment and test environment run without error, when it arrives in a production environment, the same code will fail.

This can be mostly blamed on pre-compilation. Ryan Bates does a great job of discussing this in a recent Railscast. He explains how to ensure that your capistrano scripts include steps to run pre-compile on your production server, or run it locally and send the results up with the rest of your code.

But there are other issues beyond getting production to pre-compile. You must also ensure that all of the javascript and css files you reference are set to be pre-compiled.

By default, Rails will pre-compile all non-CSS, non-JSS files, and all application.js and application.css files. But let's say you have something like this in your code:

- content_for :scripts do
  = javascript_include_tag 'signup_click'

Signup_click.js will not be pre-compiled.  So what, you say. Well, when a request is made to serve the signup_click.js file, you will get a nasty "signup_click.js isn't precompiled" error. And you will get this for any file that isn't named application.* and that is referred to directly by your erb/haml templates. If you are migrating up from 3.0, this can be a real gotcha.

So what to do.

Certainly, you should take some time to review your code, and look for specific references to css and js files and, if you can, refer to them in the application.* manifests. Invariably, you will miss some (I sure did).

So to catch them, I first attempted to run my application in production mode locally (Ryan Bates demonstrates how to do this). If you have a large site, that you have been building for a while (say, since 2.3.5), this could take a while to go through page by page.

Instead, or in addition really, I set up my cucumber environment to run in a simulated production environment. This required  a couple steps:
  1. Make sure that running all cucumber features in test mode returned no errors
  2. I added ENV["RAILS_ENV"] = "cucumber" to the top of my features/support/env.rb file (actually, within my Spork.prefork section - for those using Spork)
  3. I created a cucumber.rb environment file, copying everything from production, with the following changes. 
    1. config.serve_static_assets = true
    2. config.consider_all_requests_local       = true  
    3. config.action_controller.perform_caching = false
    4. Don't forget to disable mail 
    5. Make sure your config.assets.precompile matches production's
  4. I ran 'rake assets:precompile"
When I ran my tests, I got several failures,  all related to assets that were not precompiled. It was easy for me to find, and fix. In some cases, they were javascript/stylesheet includes/links that were redundant to what was in my application manifests, so I could just remove the include/link statements.

In other cases, they were controller specific css or js that I did not want to put into my main application manifests. At first, I included them in the config.assets.precompile statement in my production.rb environment file:

config.assets.precompile += %w( signup_click.js )

But after starting to load up this statement, I wanted to come up with a better, more flexible way.


Do these pants make me look fat (Ass-et reorganization)

Here is what I wanted to do:
  1. Not have to worry about adding files to the pre-compile list (because I know I will forget, and may not realize it until I get a production error notification)
  2. Be able to include controller specific stylesheets and javascript whenever I wanted. 
Knowing that anything listed underneath the app/assets folder named application.* would be precompiled, gave me the idea.

For each controller that I wanted a special file included when those pages were displayed, I added a folder named for the controller, and an application.css (or css.scss, or js, or js.coffee, or js.coffee.erb...but you get the picture) within that folder. I believe rails scaffolding does some of this for you, but I use rbates Nifty Scaffold, which does not.

So my file structure could look like this:
app
  assets
    javascript
      application.js
      blogs
        application.js
    stylesheets
      application.css
      blogs
        application.css

In my layout file (after the link/include tags for the base application files), I added the following lines:
= stylesheet_link_tag "#{controller_name}/application"
= javascript_include_tag "#{controller_name}/application"

This seemed to work great in development and test, but when I got to production, it failed whenever it encountered a <controller_name>/application.(js|css) that did not exist. What to do.

It occurred to me that I don't want these tags to be present if there is not an accompanying file. So I used the following helper methods to check whether the file exists.

def asset_exists?(filename, extension)
    asset_pathname = "#{Rails.root}/app/assets/"
    asset_file_path = "#{asset_pathname}/#{filename}".split(".")[0]
    !Dir.glob("#{asset_file_path}.#{extension}*").empty?
  end
  
  def js_asset_exists?(filename)
    asset_exists?("javascripts/#{filename}", "js") 
  end
  
  def css_asset_exists?(filename)
    asset_exists?("stylesheets/#{filename}", "css")
  end

and changed my include/link tags to the following:

= stylesheet_link_tag "#{controller_name}/application" if css_asset_exists?("#{controller_name}/application.css")    
= javascript_include_tag "#{controller_name}/application" if js_asset_exists?("#{controller_name}/application.js")

This worked like a charm. Whenever I wanted, I could add controller specific css and js by adding the folder and the appropriate file. I could include other files in their manifests, and/or directly add code.

If need be, a similar approach could be used for action specific files (I haven't needed to, but certainly could see doing so).

I am sure there are more elegant approaches, but this works well for me.

Any who, hope you found this helpful. And as my mother used to say "An ass-et in the hand, is better than two in the bush"

1 comment: