Jakub Suder's blog on Cocoa and web development

Extending asset pipeline with custom preprocessors

Categories: Javascript, Ruby/Rails 3 comments

If you’ve read the Rails 3.1 asset pipeline docs, you’re probably aware that you can add preprocessors to your asset files by appending extra file extensions. For example, to write your JS files in CoffeeScript you need to add the suffix .coffee, and if you also want to pass something from Rails to those files, like paths to image files, you also need to add the .erb suffix. All the extensions are added together, so you end up with e.g. profile.js.coffee.erb (it’s simpler with stylesheets, because by adding a Sass preprocessor you get a bunch of asset path helpers for free).

What the docs don’t tell you is that Sprockets can also be configured to include preprocessors implicitly based on a content type.

Writing a preprocessor

For example, let’s say you want to automatically replace every occurrence of a string HOSTNAME in your JavaScript with the actual domain name of your site. To do that you need to create a preprocessor class with a correct interface that the content will pass through:

class RootUrlPreprocessor < Sprockets::Processor
  def evaluate(context, locals)
    data.gsub(%r"\bHOSTNAME\b", "myserver.com")
  end
end

The evaluate method gets two arguments, context and locals. As far as I can tell, locals is always an empty hash, so it’s probably useless here. The context is an instance of Sprockets::Context; you can use it to e.g. read the current file’s path or content type, generate paths to asset files or access asset pipeline configuration. You also have access to several asset helpers from ActionView, like javascript_include_tag, image_tag etc.

The most important thing is the data method – it returns the contents of the asset file that you get on the input. The job of the evaluate method is to process these contents the way you want and return the processed text.

When you have that ready, you just need to tell Sprockets to apply the preprocessor to all JavaScript files (this will also include JavaScript template files, if you have any):

# in config/initializers/sprockets.rb
Rails.application.assets.register_preprocessor('application/javascript',
                                               RootUrlPreprocessor)

Now when you start the server and open a page, this code:

var socket = new WebSocket("ws://HOSTNAME/feed");

Will be coverted on the fly to:

var socket = new WebSocket("ws://myserver.com/feed");

You don’t need to add any extra extensions or anything to trigger the preprocessor – all preprocessors registered for a given MIME type will be used automatically.

One thing that might trip you up here is the asset caching feature – Rails won’t realize that after a change in the preprocessor it needs to regenerate any relevant asset files, so it will keep using the ones it has in cache. You’ll need to delete the tmp/cache/assets directory in which the cache is stored and restart the server (or alternatively, making any change in the JS file and reloading it in the browser should have the same effect).

Adding an asset path helper

I know, putting the hostname in JavaScript files isn’t very useful. Want something more useful? How about an image-path helper like the one you have in your .sass files? Simple, just make a preprocessor that will replace any occurrence of e.g. $imagePath('...') with the asset path:

class AssetPathPreprocessor < Sprockets::Processor
  def evaluate(context, locals)
    data.gsub(%r"\$imagePath\('(.*?)'\)", "'" + context.image_path($1) + "'")
  end
end

Register the new preprocessor in Sprockets for application/javascript files as in the previous example. Now you can write:

$('.start').mouseover ->
  $(this).attr 'src', $imagePath('buttons/start-active.png')

And your final JS will look like this:

$('.start').mouseover(function() {
  return $(this).attr('src', '/assets/buttons/start-active.png');
});

Of course in production mode you will get something like /assets/buttons/start-active-9934e5b1b3b2b184c5128b9987406285.png instead. And like in the previous example, the file you use it in may be called profile.js.coffee (or just profile.js if you don’t use CoffeeScript), you don’t need to add .erb – I got used to two extensions in one filename, but three is kind of too much for my taste…

3 comments:

Andrew Timberlake

Thanks for this. Inspired me to finally write the preprocessor I have been meaning to write for ages.
NOTE:
To see changes from your preprocessor, you not only have to restart rails but you also need to clean out the assets cache (rm -R tmp/cache/assets/*)

Ram

I am doing something similar to this, but need to have "assets.initialize_on_precompile = false" in order to work on heroku...

and so the initialization script that registers my processor is not happening.

when can i run this so that it will work?

Jakub Suder

@Ram: I'm not sure which files exactly get loaded when initialize_on_precompile is set to false, but config/application.rb definitely is, so you could just manually require the initializer from there.

Leave a comment

*

*
This will only be used to display your Gravatar image.

Are you a human? (yes/no) *