1. Quickstart - quick things you should know
2. `pin_all_from` - a few details
3. `pin` ...
4. Run in a console - when you need to figure stuff out
5. Relative imports - don't do it, unless you want to
6. Examples - to make it extra clear
If you're not using importmap-rails
, really, you should not have any issues. Add a file then import "./path/to/file"
. Make sure to run bin/dev
to compile your javascript if you're using jsbundling-rails
.
If you're using importmap-rails
, there is no compilation, every single file has to be served individually in development and production, and every import has to be mapped to a url for browser to fetch.
pin
and pin_all_from
is a rails way of constructing an importmap
. Imports are mapped to local files through an asset url. So just keep in mind, import "something"
could map to url /assets/file-123.js
which could map to file app/some_asset_path/file.js
or in production public/assets/file-123.js
:
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-da9b182f12cdd2de0b86de67fc7fde8d1887a7d9ffbde46937f8cd8553cb748d.js",
"@hotwired/turbo-rails": "/assets/turbo.min-49f8a244b039107fa6d058adce740847d31bdf3832c043b860ebcda099c0688c.js",
"@hotwired/stimulus": "/assets/stimulus-a1299f07b3a1d1083084767c6e16a178a910487c81874b80623f7f2e48f99a86.js",
"@hotwired/stimulus-loading": "/assets/stimulus-loading-6024ee603e0509bba59098881b54a52936debca30ff797835b5ec6a4ef77ba37.js",
"controllers/application": "/assets/controllers/application-44e5edd38372876617b8ba873a82d48737d4c089e5180f706bdea0bb7b6370be.js",
"controllers/hello_controller": "/assets/controllers/hello_controller-29468750494634340c5c12678fe2cdc3bee371e74ac4e9de625cdb7a89faf11b.js",
"controllers": "/assets/controllers/index-e70bed6fafbd4e4aae72f8c6fce4381d19507272ff2ff0febb3f775447accb4b.js",
}# ^ ^
# | |
# names you use to import urls browser uses to get it
# | ^
# | |
# `------> mapped to ---------'
}</script>
Once you have an importmap you have to import
the things you need. Importmap doesn't load anything, it is just a configuration.
Quickstart
Let's say we've added a plugin directory:
app/
└── javascript/
├── application.js # <= imports go here and other js files
└── plugin/
├── app.js
└── index.js
config/
└── importmap.rb # <= pins go here
Pin a single file:
# config/importmap.rb
pin "plugin/app"
pin "plugin/index"
# app/javascript/application.js
import "plugin/app" # which maps to a url which maps to a file
import "plugin/index"
or pin all the files in plugin directory and subdirectories:
# config/importmap.rb
pin_all_from "app/javascript/plugin", under: "plugin"
# app/javascript/application.js
import "plugin/app"
import "plugin" # will import plugin/index.js
Do not use relative imports, such as import "./plugin/app"
, it may work in development, but it will break in production.
See the output of bin/importmap json
to know what you can import and verify importmap.rb config.
Do not precompile in development, it will serve precompiled assets from public/assets
which do not update when you make changes.
Run bin/rails assets:clobber
to remove precompiled assets.
In case something doesn't work, app/javascript directory has to be in:
Rails.application.config.assets.paths
and app/assets/config/manifest.js
as //= link_tree ../../javascript .js
Pinning your files doesn't make them load. They have to be imported in application.js
:
// app/javascript/application.js
import "plugin"
Alternatively, if you want to split up your bundle, you can use a separate module tag in your layout:
<%= javascript_import_module_tag "plugin" %>
or templates:
<% content_for :head do %>
<%= javascript_import_module_tag "plugin" %>
<% end %>
# add this to the end of the <head> tag:
# <%= yield :head %>
You can also add another entrypoint in addition to application.js
, say you've added app/javascript/admin.js
. You can import it with all the pins:
# this doesn't `import` application.js anymore
<%= javascript_importmap_tags "admin" %>
Because application
pin has preload: true
option set by default it will issue a request to load application.js
file, even when you override application
entrypoint with admin
. Preloading and importing are two separate things, one does not cause the other. Remove preload
option to avoid unnecessary request.
pin_all_from(dir, under: nil, to: nil, preload: false)
Pins all the files in a directory and subdirectories.
https://github.com/rails/importmap-rails/blob/v1.1.2/lib/importmap/map.rb#L33
def pin_all_from(dir, under: nil, to: nil, preload: false)
clear_cache
@directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload)
end
dir
- Path relative to Rails.root or an absolute path.
Options:
:under
- Optional[1] pin prefix. Required if you have index.js
file.
:to
- Optional[1] path to asset. Falls back to :under option. Required if :under is omitted.
This path is relative to Rails.application.config.assets.paths.
:preload
- Adds a modulepreload link if set to true
:
<link rel="modulepreload" href="/assets/turbo-5605bff731621f9ca32b71f5270be7faa9ccb0c7c810187880b97e74175d85e2.js">
- note: either
:under
or :to
is required
To pin all the files in the plugin directory:
pin_all_from "app/javascript/plugin", under: "plugin"
# NOTE: `index.js` file gets a special treatment, instead
# of pinning `plugin/index` it is just `plugin`.
{
"imports": {
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
Here is how it all fits together:
(if something doesn't work, take your options and follow the arrows, especially the path_to_asset
part, you can try it in the console, see below)
"plugin/app": "/assets/plugin/app-04024382391bb...4145d8113cf788597.js"
# ^ ^ ^
# | | |
# :under | `-path_to_asset("plugin/app.js")
# | ^ ^
# | | |
# |.. (:to||:under)-' |
# "#{dir}/app.js" |
# '''''`-------------------------'
:to
option might not be obvious here. It is useful if :under option is changed, which will make path_to_asset
fail to find app.js.
For example, :under option can be anything you want, but :to option has to be a path that asset pipeline, Sprockets, can find (see Rails.application.config.assets.paths) and also precompile (see app/assets/config/manifest.js).
pin_all_from "app/javascript/plugin", under: "@plug", to: "plugin"
# Outputs these pins
#
# "@plug/app": "/assets/plugin/app-04024382391b1...16beb14ce788597.js"
# "@plug": "/assets/plugin/index-04024382391bb91...4ebeb14ce788597.js"
#
# and can be used like this
#
# import "@plug";
# import "@plug/app";
Specifying absolute path will bypass asset pipeline:
pin_all_from("app/javascript/plugin", under: "plugin", to: "/plugin")
# "plugin/app": "/plugin/app.js"
# "plugin": "/plugin/index.js"
#
# NOTE: It is up to you to set up `/plugin/*` route and serve these files.
pin(name, to: nil, preload: false)
Pins a single file.
https://github.com/rails/importmap-rails/blob/v1.1.2/lib/importmap/map.rb#L28
def pin(name, to: nil, preload: false)
clear_cache
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload)
end
name
- Name of the pin.
Options:
:to
- Optional path to asset. Falls back to {name}.js
. This path is relative to Rails.application.config.assets.paths.
:preload
- Adds a modulepreload link if set to true
When pinning a local file, specify name relative to app/javascript directory (or vendor or any other asset directory).
pin "plugin/app"
pin "plugin/index"
{
"imports": {
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin/index": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
Here is how it fits together:
"plugin/app": "/assets/plugin/app-04024382391bb...16cebeb14ce788597.js"
# ^ ^
# | |
# name `-path_to_asset("plugin/app.js")
# ^
# |
# (:to||"#{name}.js")-'
If you want to change the name of the pin, :to
option is required to give path_to_asset a valid file location.
For example, to get the same pin for index.js file as the one we get from pin_all_from:
pin "plugin", to: "plugin/index"
{
"imports": {
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
Run in a console
You can mess around with Importmap
in the console, it's faster to debug and learn what works and what doesn't:
>> helper.path_to_asset("plugin/app.js")
=> "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
>> map = Importmap::Map.new
>> map.pin_all_from("app/javascript/plugin", under: "plugin")
>> puts map.to_json(resolver: helper)
{
"imports": {
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
}
}
>> map.pin("application")
>> puts map.to_json(resolver: helper)
{
"imports": {
"application": "/assets/application-8cab2d9024ef6f21fd55792af40001fd4ee1b72b8b7e14743452fab1348b4f5a.js"
}
}
# Importmap from config/importmap.rb
>> Rails.application.importmap
Relative/absolute imports
Relative/absolute imports could work, if you make the correct mapping:
# config/importmap.rb
pin "/assets/plugin/app", to: "plugin/app.js"
// app/javascript/application.js
import "./plugin/app"
application.js is mapped to digested /assets/application-123.js, because ./plugin/app
is relative to /assets/application-123.js, it should be correctly resolved to /assets/plugin/app
which has an importmap that we made with our pin:
"/assets/plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
This should also just work:
// app/javascript/plugin/index.js
import "./app"
However, while import-maps
support all the relative and absolute imports, this doesn't seem to be the intended use case in importmap-rails
.
Examples
This should cover just about everything:
.
├── app/
│ └── javascript/
│ ├── admin.js
│ ├── application.js
│ ├── extra/
│ │ └── nested/
│ │ └── directory/
│ │ └── special.js
│ └── plugin/
│ ├── app.js
│ └── index.js
└── vendor/
└── javascript/
├── downloaded.js
└── package/
└── vendored.js
Output is from running bin/importmap json
:
# this is the only time when both `to` and `under` options can be omitted
# you don't really want to do this, at least not for `app/javascript`
pin_all_from "app/javascript"
pin_all_from "vendor/javascript"
"admin": "/assets/admin-761ee3050e9046942e5918c64dbfee795eeade86bf3fec34ec126c0d43c931b0.js",
"application": "/assets/application-d0d262731ff4f756b418662f3149e17b608d2aab7898bb983abeb669cc73bf2e.js",
"extra/nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin/app": "/assets/plugin/app-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"plugin": "/assets/plugin/index-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"downloaded": "/assets/downloaded-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js",
"package/vendored": "/assets/package/vendored-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
Note the difference:
pin_all_from "app/javascript/extra", under: "extra" # `to: "extra"` is implied
"extra/nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
^
pin_all_from "app/javascript/extra", to: "extra"
"nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
^
pin_all_from "app/javascript/extra", under: "@name", to: "extra"
"@name/nested/directory/special": "/assets/extra/nested/directory/special-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
^
Note the pattern:
pin_all_from "app/javascript"
pin_all_from "app/javascript/extra", under: "extra"
pin_all_from "app/javascript/extra/nested", under: "extra/nested"
pin_all_from "app/javascript/extra/nested/directory", under: "extra/nested/directory"
pin_all_from "app/javascript/extra", to: "extra"
pin_all_from "app/javascript/extra/nested", to: "extra/nested"
pin_all_from "app/javascript/extra/nested/directory", to: "extra/nested/directory"
pin_all_from "app/javascript/extra", under: "@name", to: "extra"
pin_all_from "app/javascript/extra/nested", under: "@name", to: "extra/nested"
pin_all_from "app/javascript/extra/nested/directory", under: "@name", to: "extra/nested/directory"
Same exact thing works for vendor:
pin_all_from "vendor/javascript/package", under: "package"
# etc
Single files are easy:
pin "admin"
pin "application"
pin "extra/nested/directory/special"
pin "@extra/special", to: "extra/nested/directory/special"
pin "downloaded"
pin "renamed", to: "downloaded"
When pin_all_from fails:
# if you ever tried this, it doesn't work:
# pin_all_from "app/javascript", under: "@app", to: ""
# but it can be done:
app_js = Rails.root.join("app/javascript")
app_js.glob("**/*.js").each do |path|
name = path.relative_path_from(app_js).to_s.chomp(".js")
pin "@app/#{name}", to: name
end
# useful for things like `app/components`