I am currently using the javascript_importmap_tags helper from the standard importmap-rails gem, which generates a tag for everything pinned in config/importmap.rb and does this for every page. I'd like to include only a subset of entries, grouped for the (very) isolated parts of my site, but it doesn't seem to have this capability. (It is simple to limit which things get imported but the map itself remains unnecessarily large, including everything that might get imported across the entire site.)
Justification: My importmap is over 300 entries, most of which are for my site-specific javascript modules. Within the site, almost all of these modules can be grouped into completely independent "apps". Limiting the number of entries would be worth a little bit of effort even if just for the fact that there would be less to look at when debugging. But also, generally speaking, I want to isolate things and make individual components smaller because most things are less likely to break if there are less parts, and it's often easier to make them simpler than it is to figure out whether it is a problem for them to be larger and more complicated.
How can I do this while not straying too far from the default setup? Right now I'm trying to do it by making a javascript_importmap_tags_for helper that mimics the built-in javascript_importmap_tags by making a call to javascript_inline_importmap_tag, but the actual list of mappings provided to that method usually comes from Rails.application.importmap, which is created when the server starts from config/importmap.rb. I just don't know how to make either multiple import map objects, or make one importmap object with named groups without interfering too much with the way it currently works. The docs tell you how to compose a single importmap from multiple importmap.rb files, which I've tried, but it doesn't seem to help and I don't think it was intended for this.
Should I even be trying to do this? (Will it potentially cause a problem to limit the importmap in this way and/or am I wasting effort or adding more complications trying to break it up than I'm removing by making the importmap simpler on a given page?)
Importmap::Map# app/assets/config/manifest.js
//= link_tree ../../javascript_two .js
# config/importmap_two.rb
pin_all_from "app/javascript_two"
# config/initializers/importmap_two.rb
map = Importmap::Map.new
map.draw(Rails.root.join("config/importmap_two.rb"))
map.cache_sweeper(watches: Rails.root.join("app/javascript_two"))
Rails.application.config.assets.paths << Rails.root.join("app/javascript_two")
Rails.application.class.attr_accessor(:importmap_two)
Rails.application.importmap_two = map
ActiveSupport.on_load(:action_controller_base, run_once: true) do
before_action { Rails.application.importmap_two.cache_sweeper.execute_if_updated }
end
<%= javascript_inline_importmap_tag(Rails.application.importmap_two.to_json(resolver: self)) %>
<%= javascript_inline_importmap_tag(Rails.application.importmap.to_json(resolver: self)) %>
<script type="importmap" data-turbo-track="reload">{
"imports": {
"another_app": "/assets/another_app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}</script>
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-15b16d6dc392d96a0dcc6e30c1f41abb496a3170c9c486de35da876ab020eadf.js"
}
}</script>
Importmap::Map class relies on resolver to produce digested paths. This is the only thing that can be overridden without messing with importmap internals.
# app/models/my_resolver.rb
class MyResolver
def initialize view, dir
@view = view
@dir = dir
end
def path_to_asset source, options = {}
if source.starts_with? @dir
@view.path_to_asset(source, options)
end
end
def to_json
Rails.application.importmap.to_json(resolver: self, cache_key: @dir)
end
# to get the importmap <script> tag
def importmap_tag
@view.javascript_inline_importmap_tag(to_json)
end
end
<%= MyResolver.new(self, "application").importmap_tag %>
<%= MyResolver.new(self, "controllers").importmap_tag %>
Obviously, you'll have to make sure that each importmap includes needed dependencies.
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-976dc3f309746bbfec28c82e0166c141e39eacc1727bc2fd34915c02afa0932d.js"
}
}</script>
<script type="importmap" data-turbo-track="reload">{
"imports": {
"controllers/application": "/assets/controllers/application-90dfbccab93509f543ebff5e31bf8e28a399e25e3d0cdc31a3d0669a01a614a0.js",
"controllers/hello_controller": "/assets/controllers/hello_controller-3f23212d3bfb1a2ce152d8ef84c3fc5ee29acf7b2280d300a23b502032f73420.js",
"controllers": "/assets/controllers/index-7717f93a371849ea3d8bce9f257455f80cd13dca1fc045487f376e6f8678b1b4.js"
}
}</script>
You don't have to use importmap-rails gem at all:
<script type="importmap" data-turbo-track="reload">
<%=
JSON.pretty_generate({
imports: {
"application": asset_path("application.js"),
"@hotwired/turbo-rails": asset_path("turbo.js"),
"@hotwired/stimulus": asset_path("stimulus.js"),
"@hotwired/stimulus-loading": asset_path("stimulus-loading.js"),
"controllers/application": asset_path("controllers/application.js"),
"controllers/hello_controller": asset_path("controllers/hello_controller.js"),
"controllers": asset_path("controllers/index.js"),
}
}).html_safe
%>
</script>
<script type="module">import "application"</script>
It's not really designed for this (yet) and I'd be worried that you'll miss out on some of the caching Rails wraps around this and have to update it if the internal API changes.
Are you concerned that all requests are loading un-needed JS files?
Only the modules that are actually imported in loaded code or have preload: true in importmap.rb are actually loaded (even if they show in the importmap). Any module you (or other JS) don't import or modulepreload won't be downloaded and used by the browser. So in general it's not an issue to have an unused entry in the import map. It may feel imperfect or even messy but the number of bits for the import map itself is likely lower than most of the images on your site.
It might be worth opening an issue to explore the willingness to add this though. In the meantime I'd suggest breaking up the import maps so you can customize your preloads for the divisions in your site.
First minimize your preload: true to truly global modules as much as possible.
Then in your layout replace <%= javascript_importmap_tags %> with what it expands to:
<%= javascript_inline_importmap_tag %>
<%= javascript_importmap_module_preload_tags %>
<%= javascript_importmap_shim_nonce_configuration_tag %>
<%= javascript_importmap_shim_tag %>
<%= javascript_import_module_tag "application" %>
Finally in the specific portions of your app where you want to preload some more modules for speed add those tags manually with something like:
<%= javascript_module_preload_tag(Rails.application.importmap.packages["d3-color"].path) %>
<%= javascript_module_preload_tag(Rails.application.importmap.packages["d3-time"].path) %>
You may want to content_for :head those as well unless you have multiple layouts as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With