Documentation

Import Maps

New
Browse documentation

Published 2026-08-13. V2 is a prerelease; the web core is release-gated and other previews are named separately. What beta means.

Read this page as HTML, Markdown, or structured JSON—or open the published Markdown with an AI assistant. Gemini receives the prompt through your clipboard because its signed-out page does not reliably prefill URL text; paste when the new tab opens. External assistants need the public site URL.

1.4.1
Unavailable in this version
1.5
Unavailable in this version

Import Maps

Import maps let a browser resolve a stable module name such as app to a JavaScript module. They do not require Node.js, npm, or a bundler. Asset Pipeline adds a production cache boundary by mapping logical source names to content-fingerprinted public URLs.

Supported web path: Amber CLI 2.0.6 generates one manifest-aware import map for browser-ready local modules. External modules remain an application choice with their own availability, privacy, and review boundary.

Where the examples go

Reference file map:

File tree
my_app/
├── app/assets/javascript/
│   ├── app.js
│   ├── controllers/menu.js
│   └── lib/format-date.js
├── public/assets/manifest.json                    # generated
└── src/views/layouts/application.ecr

Complete the Asset Pipeline setup first. The examples below extend its existing compiler and Amber manifest configuration.

Create the local modules

File: app/assets/javascript/controllers/menu.js — create this complete module.

JavaScript
export function connectMenu() {
  const button = document.querySelector("[data-menu-button]")
  const menu = document.querySelector("[data-menu]")

  button?.addEventListener("click", () => {
    const open = button.getAttribute("aria-expanded") !== "true"
    button.setAttribute("aria-expanded", String(open))
    menu?.toggleAttribute("data-open", open)
  })
}

File: app/assets/javascript/lib/format-date.js — create this complete module.

JavaScript
export function formatDate(value) {
  return new Intl.DateTimeFormat(document.documentElement.lang).format(value)
}

File: app/assets/javascript/app.js — create the application entry point.

JavaScript
import { connectMenu } from "menu-controller"
import { formatDate } from "format-date"

connectMenu()

for (const element of document.querySelectorAll("[data-date]")) {
  element.textContent = formatDate(new Date(element.dataset.date))
}

The entry point imports stable names, not generated digest filenames. The ECR layout owns their mapping.

Render one manifest-aware import map

File: src/views/layouts/application.ecr — place the import map in <head>, before any module script. Extend the generated import-map helper call; do not add a second map.

ECR template
<%= javascript_importmap_tag(
  {
    "app" => "javascript/app.js",
    "menu-controller" => "javascript/controllers/menu.js",
    "format-date" => "javascript/lib/format-date.js"
  },
  preload: [
    "javascript/app.js",
    "javascript/controllers/menu.js"
  ]
) %>

File: src/views/layouts/application.ecr — place the module entry point just before </body>.

ECR template
<%= content %>
<script type="module">import "app";</script>
</body>

The helper resolves each application-owned logical path through public/assets/manifest.json and emits fingerprinted URLs. Its preload values are logical asset paths, not import-map keys and not generated filenames.

Add an external module deliberately

External modules add availability, privacy, integrity, compatibility, and release-policy concerns. Prefer reviewed local modules. If a remote module earns its place, pin an exact artifact and put the external URL directly in the same map; external URLs pass through without a manifest lookup.

File: src/views/layouts/application.ecr — extend the existing map; do not render another one.

ECR template
<%= javascript_importmap_tag(
  {
    "app" => "javascript/app.js",
    "chart.js" => "https://cdn.example.invalid/chart.js@REVIEWED_VERSION/+esm"
  },
  preload: ["javascript/app.js"]
) %>

The example domain and version marker are intentionally nonfunctional. Replace them only after reviewing a real provider, exact version, browser format, license, privacy impact, and outage behavior. Self-host the reviewed module under app/assets/javascript/vendor/ when the application must work without a third-party runtime dependency.

CSS is part of the same release

An import map solves JavaScript names; it does not load styles. Keep CSS in the same authored tree and resolve it through the same manifest.

File: src/views/layouts/application.ecr — place this helper in <head>.

ECR template
<%= stylesheet_link_tag("stylesheets/app.css") %>

The compiler rewrites local url(...) references inside that stylesheet, so images and fonts receive the same content-addressed release boundary. Do not append hand-maintained ?v= values to CSS, JavaScript, images, or fonts. A byte change creates a new fingerprinted path automatically.

Build and verify

Run from: the application root.

Terminal
amber assets build
amber assets check
crystal spec
amber watch

Use View Source and the browser network panel to confirm:

  1. exactly one import map appears before the module entry point;
  2. every local mapped value is a fingerprinted /assets/ URL;
  3. every preloaded module is used and returns JavaScript;
  4. no source file imports a generated digest filename;
  5. there are no module-resolution or CSP errors; and
  6. rebuilding after a module edit changes its mapped URL.

If a strict manifest lookup fails, compare the logical value in src/views/layouts/application.ecr with the relative source path below app/assets/, then rebuild. Do not “fix” a missing entry by pasting a raw public path into the import map.

Continue with Stimulus integration for an optional controller organization pattern.