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). 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 dummy engine. It's a mock of 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 and jsbundling 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.
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.
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.
.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.
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:
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.
//= link_tree ../builds
//= link_tree ../images
//= link_tree ../svgs
And inside your engine.rb
file you can add this:
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