Gemify Your Client-Side App for Rails

Lawson Kurtz, Former Senior Developer

Article Categories: #Code, #Front-end Engineering

Posted on

Need a client-side app in a Rails project? Build it separately and include it as a gem!

If you've ever worked with a Rails app before, you've already worked with gemfiied assets.

gem "jquery-rails" is a staple of nearly every Gemfile. But while gemification of assets is traditionally restricted to common libraries like jQuery, there's no reason you can't use it for your own custom assets. In fact there are several compelling reasons to do so when you need to include a more complex client-side application into Rails.

What exactly is a gemified asset?

A gemified asset is an asset that is included into a Rails application via a gem. Gems make dependency inclusion and versioning incredibly easy. By wrapping assets in a gem, they instantly benefit from this simple dependency management.

Gemifying an Client-Side App

Make your app.

First, you create your client-side app in isolation, with it's own unique repository, and using whatever build/test systems fit the needs of that particular app. Keeping this client-side app separate from the Rails app means we aren't coerced into using the asset pipeline in weird ways just to be able to use modern front-end development tooling. You just build the app exactly how you want to.

Add a gemspec.

Once the client-side app project is established, you'll need to add a gemspec file. This is essentially a gem's equivalent of a JavaScript project's package.json. In fact you likely already have a package.json in your project already, and can choose to pull information from that file directly into the gemspec file if you'd like. (I've included an example of how to do this in the lib/gem/version.rb snippet a bit further down this page.

# your-project.gemspec

require File.expand_path('../lib/gem/version', __FILE__)

Gem::Specification.new do |spec|
  spec.name          = "your-project"
  spec.version       = YourProject::Rails::VERSION
  spec.authors       = ["Jane Doe"]
  spec.email         = ["jane.doe@viget.com"]
  spec.summary       = "I'm a client-side application!"
  spec.description   = "I'm a better description of this client-side application!"
  spec.homepage      = "https://github.com/you/your-project"
  spec.license       = "UNLICENSED"
  spec.files         = Dir["{lib}/**/*"]
  spec.require_path = "lib"
  spec.add_dependency "railties", "~> 3.1"
end

Add Rails engine registration.

Next you need to add two simple ruby files. The first will register your gem as a Rails engine, which is necessary for Rails to be able to include assets from your gem into the asset pipeline. Registration is as simple as creating a subclass of  ::Rails::Engine.

# lib/gem/engine.rb

require File.expand_path('../version', __FILE__)

module YourProject
  module Rails
    class Engine < ::Rails::Engine
    end
  end
end

In addition to registering your gem as an engine, you'll also need to define a gem version. This will be used by Rails when versioning your assets through the asset pipeline. To reduce duplication, I'd suggest pulling the version directly from package.json so you don't ever have to update your version in two places.

# lib/gem/version.rb

require 'json'
package_info = JSON.parse(File.read(File.expand_path('../../../package.json', __FILE__)))

module YourProject
  module Rails
    VERSION = package_info["version"]
  end
end

Update your build.

Rails will look for assets from registered engines (like your gem) at three different paths: app/assets, lib/assets, and vendor/assets. Update your build to place production built assets into one of these locations. (We prefer to put our custom assets in lib/assets as we use the app directory exclusively for client-side app related source code. Following Rails asset pipeline convention, JS files should be dropped into lib/assets/javascripts, and CSS files should be dropped into lib/assets/stylesheets.

The method for moving built versions of your assets into these directories will be entirely dependent on you build system. We prefer simple builds with Make, so we simply define an additional task for clearing out the old assets and replacing them with copies from our standard build output directory.

GEM_ASSETS := ./lib/assets

gem:
	rm -rf $(GEM_ASSETS)/{javascripts,stylesheets}/*
	cp $(OUT)/*.js $(GEM_ASSETS)/javascripts/
	cp $(OUT)/*.css $(GEM_ASSETS)/stylesheets/

Build and push the gem.

With your gemspec, Ruby files, and built assets in place, it's time to build the gem.

gem build your-project.gemspec

You may find it useful to establish a dedicated npm script for performing all the tasks necessary to build new versions of the gem. Ours looks like:

"scripts": {
    // ...
    "gemify": "make all && gem build your-project.gemspec"
  }

Once the gem is built, you can push it to RubyGems (or another gem repository of your choice), or simply commit it to source and push the whole project to GitHub if you'd like to serve it from there.

Including Your Gemified Asset in Rails

This is the easy part! Simply add your gem to your Gemfile.

# If your gem is hosted by RubyGems:
gem "your-project" 

# If you'd like to pull in your gem from a public GitHub repository:
gem "your-project", github: "you/your-project"

# If you'd like to pull in your gem from a private GitHub repository:
# (First make a user that only has read access to the private repo, and grab an OAuth token to use in the url below.) 
gem "your-project", git: "https://abcdefghijklmnopqrstuvwxyz:x-oauth-basic@github.com/you/your-project.git"

# If you'd like to pull in your gem from a local directory (useful for testing):
gem "your-project", path: "/path/to/your-project"

Then add your specific assets to the manifests in application.js and/or application.css.scss.

/* application.js */

//= require jquery
//= require your-project
/* application.css.scss */

/*
*= require jquery.ui.slider
*= require your-project
*/

Voila! You now have access to your gemified assets in Rails.

Updating Gemified Assets

From here, you can push new releases of your asset anytime you want to update the client-side app. 

To push a new release, make whatever changes you'd like, increment the version number in package.json, run your gem build command (e.g. npm run gemify), and push it to your gem repository.

On the Rails side, you can choose to include a specific version of the gem in your Gemfile, or you could update to the latest available version via bundle update --source your-project. Since Rails asset versioning is tied to your gem version, you never have to worry about cache invalidation. You're done.

Pros & Cons

So why would you want to gemify an asset in the first place? And what are the caveats?

Pros

The huge advantage of gemification for client-side apps is that separation allows them to be treated as proper applications, not just side-kicks of a Rails application. This means your client-side app can have its own repo, with its own contributors, working with its own (better) build tools, protected by its own test suite, and with its own, isolated deployment process. In most cases, the concern addressed by the client-side app is completely distinct from the main concerns of your Rails app, so it makes technical sense to isolate these concerns from one another.

While there are many ways to separate a client-side app from a Rails app, gemification is probably the easist to manage because of how easy gems make deployment and versioning. I dare you to find an easier, more reliable, and more flexible way to include external assets into Rails.

Cons

Joining separate systems is inherently complex. If your needs are simple, or are well met by the asset pipeline, splitting your asset into a separate gem probably won't be worth the squeeze.

Sharing code also becomes more complicated when you separate concerns. Some extra though is required about what belongs where, and how best to share common logic like Sass color values, etc.

Finally, with separate build systems in place for various assets, it's possible to include multiple versions of a shared library that otherwise should have only been loaded once. If this is a concern, consult your build tool documentation about how to construct builds to rely on shared, external libraries.

Summary

So there you have it. We've enjoyed including client-side apps into larger Rails applications via gemification. Hopefully you will too! Let us know what you think in the comments below.

Related Articles