Rails 8 Assets: Combining importmaps
This post is part of a mini series on Rails 8 asset pipeline. For the full picture, start with breakdown of how propshaft and importmap-rails work together and Propshaft deep dive.
Recap of importmap-rails gem
The import
statement in JavaScript modules allows you to import functionality from other module files. However, this typically requires providing URLs to the other JavaScript module sources. Importmaps simplify this process by letting you use shorter names instead of full URLs. This increases readability and makes it easier to relocate files, such as moving them to a CDN, without rewriting the import
statements.
The importmap-rails
gem supports defining an importmap using a neat DSL:
1
2
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
This produces the following importmap in your HTML:
1
2
3
4
5
6
<script type="importmap">{
"imports": {
"application": "/assets/application-f0907bdc.js",
"@hotwired/turbo-rails": "/assets/turbo.min-fae85750.js"
}
}</script>
Why would you need to combine them?
Your rails application has its importmap.rb
and if you’re using Rails engines, each will get its own importmap.rb
file with its own definitions.
I was setting up my demo site and I wanted to isolate each demo into a namespace so that I can have all its files conveniently separated. I wanted to make the engine’s importmap extend the main application’s one. This was the issue that sent me down the path of reading the source of Propshaft and importmap-rails.
How to combine them
Key to doing that is understanding how the gem evaluates importmap.rb
: it is loaded and instance_eval
‘d in the context of an Importmap::Map
instance. It exposes a draw
method which does exactly that (source code). The DSL of importmap.rb
is essentially calling methods on the instance of Importmap::Map
. As you’re calling the pin
method it is building an internal Hash
with all the entries it will later output into the HTML.
All we need to do is to call draw
twice, with the two importmap source files, but on the same importmap instance:
1
2
3
myImportmap = Importmap::Map.new
myImportmap.draw("./importmap1.rb"))
myImportmap.draw("./importmap2.rb"))
The gem provides a javascript_importmap_tags
helper function that renders all the necessary script tags. It’s not mentioned in the readme but it accepts an optional importmap
parameter which allows you to override the default importmap map it uses:
1
<%= javascript_importmap_tags, importmap: myImportmap %>
How to make Rails engine importmap extend the Applications’s one
We need to:
- Build a custom importmap.
- Store it somewhere.
- Use it in the layout.
There are multiple ways you can go about this but this is the simplest one I found. Inline comments explain the setup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module MyEngine
class << self
# Place it directly on the engine
attr_accessor :importmap
end
class Engine < ::Rails::Engine
# Set it up in a new initializer
initializer "morphing.importmap", before: "importmap" do |app|
Morphing.importmap = Importmap::Map.new
# Evaluate the main application's importmap
Morphing.importmap.draw(app.root.join("config/importmap.rb"))
# Evaluate the engine's importmap
Morphing.importmap.draw(root.join("config/importmap.rb"))
end
end
end
Now use it in the engine’s layout file:
1
2
3
4
5
<%=
javascript_importmap_tags
"my_engine/application",
importmap: Engine.importmap
%>
See a real-world implementation in my demo application.
In the next article I’ll explain how to combine bundled with importmap assets, i.e. how to keep using the default asset pipeline but still be able to use more complicated bundled packages from npm. Subscribe below to not miss it.
Use cases for combined importmaps
This approach is also useful for:
- Separate site sections: Load specific JS files only when users visit admin or back office sections.
- Complex UI components: Preload heavy JS files only when specific components appear on screen. The
pin
method supportspreload: false
, which prevents preloading a source file until theimport
statement executes. That will avoid loading it on other pages but it will make the page with complex UI load slower. Instead, you could extend the main importmap to have it preload everything only on the page where the complex UI is present.
It’s a flexible tool, go crazy, live a little.