Import Maps Under the Hood in Rails 7
Tags: ruby, rails
Published: 2022-03-16 13:15:12

Import maps is the new feature in Rails 7 that allows us to say goodbye to Node.js and tools like Webpack. There's no need for bundling anymore. With this new mechanism, you can still manage your JavaScript libraries with a specific version. Instead of one big file, though, your application serves many small JavaScript files.

It’s essential you know how import maps work to benefit from the newest version of Rails (but don’t worry, you can still use tools like Webpack instead). However, it’s also worth discovering what is happening under the hood. This way, you can better understand the journey from installing a JavaScript library in your project to serving its content to your users.

This article will show you how to install, serve, and uninstall JavaScript libraries with import maps and what happens under the hood during each phase.

## The Core of Import Maps

The feature itself is not complicated. However, before I show you what happens inside the library, I would like to introduce the JSPM tool. JSPM is a shortcut for JavaScript Package Management. Thanks to this tool, you can load any NPM package inside the browser without extra tooling, and it will be fully optimized.

Rails uses JSPM to serve JavaScript libraries in your application. You can either download the source files to the vendor directory or serve the code directly from the URL.

For example, to access the jQuery library, you can call [https://api.jspm.io/generate?install=jquery](https://api.jspm.io/generate?install=jquery) URL in your browser, and you will receive the URL to the minified source code in the JSON response. Of course, the service provides some more options for requests, but this knowledge is enough to understand how Rails cooperates with JSPM in the import maps library.

## Install Import Maps in Your Rails Project

The import maps feature is available in Rails as a Ruby gem. If you generate a new project with Rails 7, it’s included in the Gemfile by default. You can add it to existing projects by executing the following command:

```shell
bundle add importmap-rails
```

Once you have the gem installed, you have to generate the required files by using the following command:

```shell
./bin/rails importmap:install
```

What does this command do, exactly? First, it calls the install rake task included in the importmap namespace. The gem delivers the rake task. Inside the task, the standard rails rake task is executed:

```ruby
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/install.rb",  __dir__)}"
```

The `install.rb` file does a few things in the following order:

1. **Adds helper to the layout in the head section** - if the `application.html.erb` layout exists in your project, it adds the `javascript_importmap_tags` line before the closing `</head>` tag. This helper includes JavaScript libraries pinned by import maps. If your project does not include the standard layout file, it will render the information about the helper so you can add it by yourself.
2. **Creates app/javascript/application.js file** - it adds the comment about import maps to let you know that there is a new place where you should define dependencies.
3. **Creates vendor/javascript directory** - this is where JavaScript libraries will be stored if they are not served directly via a URL.
4. **Updates sprockets manifest, if it exists** - sprockets needs to know about the JavaScript files placed inside the `vendor/javascript` directory.
5. **Creates config/importmap.rb file** - this will contain the list of libraries used by Rails (something like package.json file).
6. **Copies bin/importmap file and sets the correct permission to execute it** - this file is used to pin and unpin specific libraries.

As soon as this process finishes, you are ready to install JavaScript libraries using the `./bin/importmap` file, and Rails is prepared to serve those files to your visitors.

## Adding Libraries to Your Rails Project

When installing a library with import maps, you have two options: you can either use the code directly from the CDN URL or download the file and serve it directly from your server.

Let’s explore the CDN option first.

### Using NPM Packages via JavaScript CDN’s

It all starts with the pin command and the library name:

```shell
./bin/importmap pin jquery
```

You pass two arguments to the program located inside the `./bin/importmap` file. `importmap` is an executable file that loads a `config/application.rb` file from your project and the import maps commands file.

Import maps use the Thor gem to handle command line programs gently. The first argument, pin, is the command name. When you invoke it, the following things happen under the hood:

- **Request to JSPM API is executed**

As I mentioned before, the gem uses JSPM to get the contents of the package into our application. Therefore, the very first step is the request URL formation. Without any extra parameters passed to the pin command, the request URL is [https://api.jspm.io/generate](https://api.jspm.io/generate) with the following parameters:

- `install` - ["jquery"] - the param is an array because you can install multiple packages simultaneously.
- `flattenScope` - true - with this param set to `true`, the returned import map format will be more straightforward without scopes, if possible.
- `env` - ["browser", "module", "production"] - this is the list of environment condition strings.
- `provider` - "jspm" - besides JSPM, Skypack, JSdelivr, and Unpkg providers are available to use as well.

Try accessing [https://api.jspm.io/generate?install=jquery&flattenScope=true](https://api.jspm.io/generate?install=jquery&flattenScope=true) in your browser, and you will get a simple JSON response with some simple attributes.

- **Response from JSPM is parsed**

Parsing is a very straightforward phase. As you saw, the response is simple, and all we need is the library name and CDN URL. The `Packager` class is responsible for parsing the response in the import map library. Instead of returning the attributes, it returns a complete line that you can use directly in the config:

```ruby
%(pin "#{package}", to: "#{url}")
```

If you are not yet familiar with `%()`, don’t worry, as it works almost the same as the following:

```ruby
"pin \"#{package}\", to: \"#{url}\""
```

The only difference is that the `%()` notation escapes the double quotes automatically. The generated config line passes to another function that handles the config file update process.

**Import map config is updated**

The last step is to update the config file with the pin definition. Because the previous pin definition can be present, the gem first verifies the config file and searches for the existing library definition. It’s simple to do with the regex:

```ruby
/^pin "#{package}".*$/
```

If the pin definition for the given package is present, the gem replaces the line with the new definition. If the previous definition is not present, the gem adds a new line at the end of the `config/importmap.rb` file.

**Note**: When you use this method, keep in mind that this option is free, but one person supported by the community manages the JSPM servers. This situation could change in the future, so I advise you to store libraries locally if you plan to use import maps with production-ready applications.

### Downloading NPM Packages

If you don’t want to use source code from a CDN URL, you can download the library to the `vendor` directory inside your application. In this case, the process is very similar to the case with CDN URLs. What's different is that the code is downloaded into a .js file before the config line is returned for an update.

**The download process**

The download process consists of a few small steps:

- The gem calls `FileUtils.mkdir_p`, which creates the `vendor` directory if it does not already exist.
- The gem calls `FileUtils.rm_rf` to remove the previous package file if it exists.
- The gem saves JS code located under the JSPM-provided URL into the .js file and places it into the `vendor` directory.

If the gem cannot download the package’s contents, you will be notified by a proper error raised in your command line.

**Config line generation**

If the package is named the same as the file with the package source, the config line is straightforward:

```ruby
%(pin "#{package}" # #{version})
```

Otherwise, the import map needs to map the package name to the file directly:

```ruby
%(pin "#{package}", to: "#{filename}" # #{version})
```

When the config line is formatted and returned, the config is updated with the package definition (as described in the CDN URL version above).

## Using Pinned Packages in Your Rails App

The gem extensively uses the Rails engine mechanism to deliver features. The `Importmap::Engine` defines a bunch of initializers that perform the configuration.

### Import Pinned Packages from config/importmap.rb

The configuration lines inside the config/importmap.rb file are replaced with references inside the application. First, the gem collects all definitions into a hash where the `Struct` object represents each definition to make it easier to access specific attributes.

You can call `Rails.application.importmap.packages` to access the hash with all definitions inside the application.

### Including References to Packages inside Views

When pinned packages are imported, you can include them in your views to provide the code. The gem provides a `javascript_importmap_tags` helper, which you can simply render in your layout. It uses `Rails.application.importmap` to generate JSON output for all pinned libraries, and with the usage of `asset_helper` in Rails, it provides the correct paths to the libraries.

After adding the following line to your `head` section of the layout:

```html
<%= javascript_importmap_tags %>
```

You will get the following output (assuming that you are only using jQuery):

```html
<script type="importmap" data-turbo-track="reload">
  {
    "imports": {
      "application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
      "jquery": "https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js"
    }
  }
</script>
```

### Handling Import Maps by Browser

From that moment on, the browser handles the rest. If you are not familiar with the `importmap` script type, let me explain it quickly.

Import map simply controls what is fetched when you use the following statement inside your JavaScript code:

```javascript
import "jquery";
```

## Removing Pinned Packages

To remove a pinned package, you have to execute the unpin command:

```shell
./bin/importmap unpin react
```

You don’t have to pass the `--download` parameter because the gem will automatically delete any files related to the package. The removal process consists of three steps:

- **Request to the JSPM API** - the same request is executed when the package is added. The gem does this to get up-to-date package information.
- **Removal of the existing package files** - the gem uses `FileUtils.rm_rf` to remove all related files even if they are placed in directories.
- **Remove the config lines related to the package** - with the usage of `File.readlines`, the gem loads all lines from the config file, selects those that do not contain anything related to the removed package and saves them again in the config file. Simple as that.

You can also unpin multiple packages at once; just add their names after the `unpin` argument.

## Wrap Up

We've reached the end of this short, yet valuable, journey. You now know that import maps is just a different way of loading JavaScript libraries in your web application. Instead of one big bundled file, you serve multiple smaller files that are easy to cache and control.

With the `importmap-rails` gem, it’s easy to adapt import maps in your project. The gem ships with a simple configuration file and command-line interface to install and remove packages using the JSPM.

Should you use import maps in your next Rails project? As always, it depends. The good thing is that you are not limited to import maps — you can always switch between it, Webpack, and similar, more complex tools.

Thanks for reading and happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**