Cake walk: Using bower with rails

Yuva's avatar

Yuva

Traditionally, in order to use any javascript library (js lib), Rails users will do either of these two things:

  • Directly copy paste js lib source into vendor/assets/javascript folder
  • Create a gem, add js lib source to gem, and reuse that gem.

First option is problematic because users have to keep track of changes to js lib, and update them whenever required. Second option delegates this responsibility to gem author, and users will just bump up gem version and most of the times assume that latest gem will have latest js lib. Both these approaches have problems because everytime js lib author improves lib either users have to copy sources or gem authors have to make a new release.

Of late, creating js libs and distributing them through bower has gained lot of traction. There are different ways to use bower with Rails. A popular way is to use bower rails gem. This blog will not use this gem, but explores sprockets inbuilt support for bower.

Sprockets - Bower support

Sprockets has support for bower. It doesn't do package management on its own, but it can understand bower package structure, and pick up js, and css files from bower package. Lets go through this simple example:

  • Create a simple rails app. Just use rails new, nothing new here.
  • Install Faker js lib and use it in rails app.
Setting up bower json file

Packages installed from bower need to be specified in a bower.json file. Run bower init command at the root of rails app to generate this file. This file will be checked into version control, so that other devs can also know about dependencies.

> bower init
? name: rails app
? version: 0.0.0
? description:
? main file:
? what types of modules does this package expose?:
? keywords:
? authors: Yuva <yuva@codemancers.com>
? license: MIT
? homepage: rails-app.dev
? set currently installed components as dependencies?: No
? add commonly ignored files to ignore list?: No
? would you like to mark this package as private which prevents
  it from being accidentally published to the registry?: Yes
 
{
  name: 'rails app',
  version: '0.0.0',
  homepage: 'rails-app.dev',
  authors: [
    'Yuva <yuva@codemancers.com>'
  ],
  license: 'MIT',
  private: true
}
 
? Looks good?: Yes

One of the important things to note here is to mark this package as private so that its not published by mistake. The generated bower.json file can be further edited, and un-necessary fields like homepage, author can be removed.

{
  "name": "rails app",
  "version": "0.0.0",
  "private": true
}

Note: This is a one time process.

Setting up .bowerrc file

Since rails automatically picks up assets from fixed locations, bower can be instructed to install packages to one of the pre defined locations. Create a .bowerrc file like this:

{
  "directory": "vendor/assets/javascripts"
}

Since bower brings in third party js libs, its recommended to put them under vendor folder. Note: This is a one time process

Installing Faker js lib and using it

Use bower install to install above said lib. Since .bowerrc defaults the directory to vendor/assets/javascript, Faker will be installed under this directory. Use --save option with bower to update bower.json.

> bower install Faker --save

Thats it! Faker lib is installed. Add an entry in application.js and use the lib.

//= require Faker

Note: Make sure that its just Faker and not Faker.js. More details on why no extension will be explained later in the blog post.

What just happened? How did it work?

The vendor/assets/javascript folder has a folder called Faker, and that folder does not have any file called Faker.js. Inspecting any page from the rails app, script tab looks like this:

Firefox inspect script tab

Looking at source code of Faker, there is a file called faker.js, and is under build/build folder. How did rails app know the location of this file, even though application.js doesnot have explicit path? This is where sprockets support for bower kicks in:

  • Sprockets will search for Faker in asset paths.
  • If found, it checks whether its a file or a directory. In the rails app, bower installed this lib under directory Faker.
  • If directory, it checks if the directory contains bower.json file. In the rails app, Faker folder does have bower.json file.
  • It decodes json file, and parses main field containing files and returns the desired assets.

Now, bower.json for Faker has explicit path for faker.js, which will be returned by sprockets

{
  "name": "faker",
  "main": "./build/build/faker.js",
  "version": "2.0.0",
  # ...
}
Bonus: Digging into sprockets source code

First, sprockets will populate asset search paths. When sprockets sees require Faker in application.js, it checks for extension, and since there is no extension, ie .js, asset search paths will be populated with 3 paths:

  • Faker/.bower.json
  • Faker/bower.json
  • Faker/components.json

Gist of the source:

  def search_paths
    paths = [pathname.to_s]
 
    # optimization: bower.json can only be nested one level deep
    if !path_without_extension.to_s.index('/')
      paths << path_without_extension.join(".bower.json").to_s
      paths << path_without_extension.join("bower.json").to_s
      # DEPRECATED bower configuration file
      paths << path_without_extension.join("component.json").to_s
    end
  end

Source code is here: populating asset paths

Second, while resolving require Faker, if bower.json file is found, sprockets will parse the json file, and fetches main entry. Gist of the source:

  def resolve(logical_path, options = {})
    args = attributes_for(logical_path).search_paths + [options]
    pathname = Pathname.new(path)
    if %w( .bower.json bower.json component.json ).include?(pathname.basename.to_s)
      bower = json_decode(pathname.read)
      yield pathname.dirname.join(bower['main'])
    end
  end

Source code is here: resolving bower json