Cake walk: Using bower with rails

Yuva  - December 10, 2014  |   , , ,

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

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:

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:

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:

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