Tag Archives: rails 3

Rails 3 Engines/Plugins and Static Assets

THIS INFORMATION IS OUT OF DATE!  Since Rails 3.2, you can easily include static assets in your engine using the “app/assets” folder.  This post is only applicable for older versions of Rails, and still not suggested for production systems.

Engine-ish

One of the big changes in Rails 3 is the move to “everything is an engine”.  You end up reading a lot about this in relation to plugins, mentioning how this architecture makes it so you can easily embed entire rails applications into others since they’re all just derivatives of Railties.  While this holds true for very simple cases, you’ll quickly find simply creating a rails app, and sticking it in vendor/plugins doesn’t work the way you’d expect from reading about it.  You really do need to create your plugin in a certain way to actually make it work.  Over here is a good writeup on creating a Rails Engine based plugin installed as a gem.

Serving Static Assets

Now, serving static assets from your plugin is a bit on the unintuitive side, and likely for a good reason.  You generally do not want to do this for any kind of production system. However, like most rules, there are times when breaking them makes pragmatic sense.  In this use case we have an internal tool that we want to be able to easily extend.  The maximum number of concurrent users will likely be in low teens, and serving static assets through the rails app is a non-issue.  Extending the app includes providing views, controllers, models, routes, and images.  The first three are extremely straightforward and intuitive, routes use a slightly different enclosing syntax in an Engine, but are otherwise identical.  Images, however are a bit tricky.

There are two ways to create an Engine based plugin, and they are a bit incompatible with each other.  You can either install your plugin as a gem, or as a plugin in the app.  In both cases, you are going to use the ActionDispatch::Static middleware to serve your content. I am also assuming that you are placing your content in the “public” directory in the root of your plugin’s file structure.

Gem Based Plugin

A gem based plugin is initialized via the definition of itself in your plugin’s lib directory.  You can see an example of this below.  In order to set it up correctly you should put your middleware line in an initializer in your declaration:

module MyEngine
  class Engine < Rails::Engine
    initializer "static assets" do |app|
      app.middleware.use ::ActionDispatch::Static, "#{root}/public"
    end
  end
end

In App Plugin

Unfortunately, if the above file exists in a plugin installed to vendor/plugins your app will fail to load.  This is because rails automatically assumes all plugins are Engines, and initializes them as instances of the Plugin class which inherits from Engine.  It will autoload everything in lib, see the declaration, and then fail out with the incredible error message “[Your Plugin] is a Railtie/Engine and cannot be installed as plugin”. Which is somewhat misleading since a plugin is an Engine, just not explicitly.

To get around this issue, you just have to move where you do your initialization and setup. Inside your plugin’s init.rb (which should be in the root of your plugin’s file structure) is where you can do this kind of stuff.  Rails boots your Plugin, and then loads this file with the variable “config” set for you to do your work.  So it’s simply:

config.middleware.use ::ActionDispatch::Static, "#{root}/public"

Load Order

I strongly recommend using middleware.use as it will place your middleware after the parent app, and therefore will cause your plugins assets to be of lower priority than everything else in the stack. This means that files in the parent app’s public directory will load instead of the plugin’s in the case of a conflicting name.  You still have options though.  To give your plugin’s static files priority over everything except the parent app’s static files, use:

middleware.insert_after ::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public"

To give your plugin top priority in static asset serving use:

middleware.insert_before ::ActionDispatch::Static, ::ActionDispatch::Static, "#{root}/public"