Post

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:

  1. Build a custom importmap.
  2. Store it somewhere.
  3. 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 supports preload: false, which prevents preloading a source file until the import 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.

This post is licensed under CC BY 4.0 by the author.