# Rails Engine Assets: Making Your Gem Work with Sprockets AND Propshaft

The golden rule for libraries is to support integration with as many parent apps as possible because you want to cover as much as you can of the full spectrum of customers.

Back in 2022 there was a "divide". Well, not really a divide per se but a new thing introduced which improves on the old thing.
The old thing is sprockets and the new one is propshaft.

This meant that the plethora of apps who were using sprockets should now upgrade. All the engineers who were using it now need to learn the differences of the two, why it's important to switch, how to do it, and its particularities.
That's why I call it "the divide".

Another rather important aspect is that library authors need to figure out how to enable their libraries to work on these new propshaft apps.

That's what we had to do with [Avo (an admin panel framework for Ruby on Rails)](https://avohq.io). We wanted the new propshaft folks to be able to use it without encumbrances and with the least amount of impact as possible.

So let's get into it!

### Which one should I use?

The answer to this question is a simple one. Propshaft!
That's the future! I won't go into what it does as that's a different post.

### Why would anyone use sprockets now?

Because of historical reasons. Their app was first generated with it and they haven't got around to changing it.
That's fine, the cost of change is quite high.

### How to support both Sprockets and Propshaft?

The simple answer is to set up specific configurations for both of them.

We'll do a lot of work inside the `engine.rb` file. That's the entrypoint into how to hook up into an app through from a Rails Engine.

We'll work in this [confsmith](https://github.com/avo-hq/confsmith) dummy engine. It's a mock of [marksmith](https://github.com/avo-hq/marksmith) which is a GitHub-styled Markdown editor for Ruby on Rails.

### Are we bundling our assets?

For this tutorial we will. We'll use [cssbundling](https://github.com/rails/cssbundling-rails) and [jsbundling](https://github.com/rails/jsbundling-rails) gems.
We want to cover as many use-cases as possible and this approach is the best. It gives us the best balance between power and versatility.

So let's add the gems to the app.
Unfortunately, the generators don't work inside engines so we need to add it to the app and move the files over.

```bash
bundle add jsbundling-rails cssbundling-rails

cd test/dummy

rails javascript:install:esbuild
rails css:install:tailwind

# Manually move the generated files from the dummy app to the gem
```

Now we are working in a modern CSS and JS environment.
When we run `yarn build` and `yarn build:css` we'll have the compiled (bundled) CSS and JS files inside our `app/assets/builds` directory, which is perfect!

As a general rule of thumb, anything that lives in the `builds` directory will be available to the app asset pipeline.

The tricky thing is to still have a few things which aren't in the `builds` directory. The reason why they aren't in the builds directory is mostly for Developer Experience (DX).
Those files are the `application.css`, `application.js` (and other CSS and JS files), images, fonts, SVGs and more which you'd want in different directories when you develop the app/gem.

So, we need to expose them to the Rails app.

### Expose CSS and JS files to a Rails app from an Engine

As we mentioned, the CSS and JS files which we built in the `builds` directory, are automatically exposed to both Sprockets and Propshaft.
That's a done job!

### Expose images and SVGs to a Rails app from an Engine

In our `engine.rb` file we should create an initializer callback, which behaves the same as the initializer file in your `config/initializers` directory and register those directories.

```ruby
module Confsmith
  class Engine < ::Rails::Engine
    isolate_namespace Confsmith

    initializer "confsmith.assets" do |app|
      if app.config.respond_to?(:assets)
        # Add Confsmith's assets to the asset pipeline
        app.config.assets.paths << Engine.root.join("app", "assets", "builds").to_s # this shouldn't be necessary but I like to future-proof it to be on the safe side of things
        app.config.assets.paths << Engine.root.join("app", "assets", "images").to_s # this will expose the images directory
        app.config.assets.paths << Engine.root.join("app", "assets", "svgs").to_s # this will expose the svgs directory
      end
    end
  end
end
```

Now, whenever someone calls `<%= image_tag 'confsmith/logo.png' %>` it will work on both Sprockets and Propshaft apps and will use the logo in the `app/assets/images/confsmith` directory.
Same with SVG files.

### Expose assets in CSS to a Rails app from an Engine

The next thing we might need is `url` support in CSS.

We might have this file in `app/assets/images/confsmith/logo.png` and we want to use it in our CSS.

```css
.logo {
  background-image: url('confsmith/logo.png');
}
```

Propshaft is pretty smart and will find it because of this line `app.config.assets.paths << Engine.root.join("app", "assets", "images").to_s`.

It will turn that `url('confsmith/logo.png')` into `url('/assets/confsmith/logo-HASH.png');`.
It's so smart that you'll be able to use it without the prefix if you want to `url('logo.png');` and it will still work. Caution with that approach because it might have a conflict with other files in the parent app, so I recommend you use the engine name prefix.

Sprockets is a bit different.
You have to give it the prefix and you have to expose the file to the precompile path in one way or another.

#### 1. Expose using the `app.config.assets.precompile` array

This is the "code" approach of exposing files to Sprockets.
The most basic thing you can do is this one to append one file at a time to the `precompile` array.

```ruby
module Confsmith
  class Engine < ::Rails::Engine
    isolate_namespace Confsmith

    initializer "confsmith.assets" do |app|
      if app.config.respond_to?(:assets)
        app.config.assets.precompile += %w[ confsmith/logo.png ]
      end
    end
  end
end
```

Now, Sprockets will take that file and add it to the precompile path and have it available to the app as an HTML asset (`<%= image_tag 'confsmith/logo.png' %>`) and as a CSS and JS asset (`url('confsmith/logo.png')` in your CSS).

A more programmatic approach of exposing files to Sprockets would be to do this:

```ruby
module Confsmith
  class Engine < ::Rails::Engine
    isolate_namespace Confsmith

    initializer "confsmith.assets" do |app|
      if app.config.respond_to?(:assets)
        # Configure asset precompilation for Confsmith assets
        asset_paths = [
          ["app", "assets", "images"],
          ["app", "assets", "images", "confsmith"],
          ["app", "assets", "svgs"],
        ]

        paths_to_precompile = asset_paths.flat_map do |path|
          Dir[Engine.root.join(*path, "**", "*")].filter_map do |file|
            # Skip directories - Dir.glob can match directories with ** pattern
            next unless File.file?(file)

            # Get relative path from the assets directory using cross-platform path handling
            Pathname.new(file).relative_path_from(Engine.root.join(*path)).to_s
          end
        end

        app.config.assets.precompile += paths_to_precompile
      end
    end
  end
end
```

What this script does is it takes all the files in the given directories and adds them to the precompile path using a relative path from those given directories, and that's important to sprockets.

For example, if you have a file in `app/assets/images/confsmith/logo.png` and you add it to the precompile path you need to give it as `confsmith/logo.png`, not `logo.png` or a full path like `app/assets/images/confsmith/logo.png` or an absolute path like `/Users/adrian/work/confsmith/app/assets/images/confsmith/logo.png`.
It's a tricky thing that's not documented anywhere and I had to figure it out by myself 😅.

So this script does just that.

#### 2. Expose using the `manifest.js` file

This is a more classic approach that you might recognize if you've been using Rails for a while.

You can create a `confsmith_manifest.js` file in the `app/assets/config` directory and add the files you want to expose to the asset pipeline.

```js
//= link_tree ../builds
//= link_tree ../images
//= link_tree ../svgs
```

And inside your `engine.rb` file you can add this:

```ruby
module Confsmith
  class Engine < ::Rails::Engine
    isolate_namespace Confsmith

    initializer "confsmith.assets" do |app|
      if defined?(::Sprockets)
        # Tell sprockets where your assets are located
        app.config.assets.precompile += %w[confsmith_manifest.js]
      end
    end
  end
end
```

What we did was to tell Sprockets to precompile the `confsmith_manifest.js` file which in turn knows how to target those directories in a very Sprockets-y way.

### Best practices and recommendations

##### 1. Namespace your directories with the engine name

Whenever possible, especially with the final files, you should namespace the directory they are in with the engine name.

Place your final (bundled) `application.css` and `application.js` files in the `app/assets/builds/ENGINE_NAME` directory.
Place your images in `app/assets/images/ENGINE_NAME` and your SVGs in `app/assets/svgs/ENGINE_NAME`.

This way you'll be able to use the engine name prefix in your CSS and JS files and your images and SVGs will be available to the app asset pipeline.

Comments are open on [HackerNews](https://news.ycombinator.com/item?id=44681747)
