{"title":"Import Maps","description":"Map browser-native JavaScript modules through Amber's fingerprinted asset manifest","section":"guides/assets","version":"v2","path":"guides/assets/import-maps","canonical_url":"https://amberframework.org/docs/v2/guides/assets/import-maps","markdown_url":"https://amberframework.org/docs/v2/guides/assets/import-maps.md","inherited":false,"content_markdown":"# Import Maps\n\nImport maps let a browser resolve a stable module name such as `app` to a\nJavaScript module. They do not require Node.js, npm, or a bundler. Asset\nPipeline adds a production cache boundary by mapping logical source names to\ncontent-fingerprinted public URLs.\n\n> **Supported web path:** Amber CLI `2.0.6` generates one manifest-aware import\n> map for browser-ready local modules. External modules remain an application\n> choice with their own availability, privacy, and review boundary.\n\n## Where the examples go\n\n**Reference file map:**\n\n```text\nmy_app/\n├── app/assets/javascript/\n│   ├── app.js\n│   ├── controllers/menu.js\n│   └── lib/format-date.js\n├── public/assets/manifest.json                    # generated\n└── src/views/layouts/application.ecr\n```\n\nComplete the [Asset Pipeline setup](../) first. The examples below extend its\nexisting compiler and Amber manifest configuration.\n\n## Create the local modules\n\n**File: `app/assets/javascript/controllers/menu.js` — create this complete\nmodule.**\n\n```javascript\nexport function connectMenu() {\n  const button = document.querySelector(\"[data-menu-button]\")\n  const menu = document.querySelector(\"[data-menu]\")\n\n  button?.addEventListener(\"click\", () => {\n    const open = button.getAttribute(\"aria-expanded\") !== \"true\"\n    button.setAttribute(\"aria-expanded\", String(open))\n    menu?.toggleAttribute(\"data-open\", open)\n  })\n}\n```\n\n**File: `app/assets/javascript/lib/format-date.js` — create this complete\nmodule.**\n\n```javascript\nexport function formatDate(value) {\n  return new Intl.DateTimeFormat(document.documentElement.lang).format(value)\n}\n```\n\n**File: `app/assets/javascript/app.js` — create the application entry point.**\n\n```javascript\nimport { connectMenu } from \"menu-controller\"\nimport { formatDate } from \"format-date\"\n\nconnectMenu()\n\nfor (const element of document.querySelectorAll(\"[data-date]\")) {\n  element.textContent = formatDate(new Date(element.dataset.date))\n}\n```\n\nThe entry point imports stable names, not generated digest filenames. The ECR\nlayout owns their mapping.\n\n## Render one manifest-aware import map\n\n**File: `src/views/layouts/application.ecr` — place the import map in `<head>`,\nbefore any module script. Extend the generated import-map helper call; do not\nadd a second map.**\n\n```ecr\n<%= javascript_importmap_tag(\n  {\n    \"app\" => \"javascript/app.js\",\n    \"menu-controller\" => \"javascript/controllers/menu.js\",\n    \"format-date\" => \"javascript/lib/format-date.js\"\n  },\n  preload: [\n    \"javascript/app.js\",\n    \"javascript/controllers/menu.js\"\n  ]\n) %>\n```\n\n**File: `src/views/layouts/application.ecr` — place the module entry point just\nbefore `</body>`.**\n\n```ecr\n<%= content %>\n<script type=\"module\">import \"app\";</script>\n</body>\n```\n\nThe helper resolves each application-owned logical path through\n`public/assets/manifest.json` and emits fingerprinted URLs. Its `preload` values\nare logical asset paths, not import-map keys and not generated filenames.\n\n## Add an external module deliberately\n\nExternal modules add availability, privacy, integrity, compatibility, and\nrelease-policy concerns. Prefer reviewed local modules. If a remote module earns\nits place, pin an exact artifact and put the external URL directly in the same\nmap; external URLs pass through without a manifest lookup.\n\n**File: `src/views/layouts/application.ecr` — extend the existing map; do not\nrender another one.**\n\n```ecr\n<%= javascript_importmap_tag(\n  {\n    \"app\" => \"javascript/app.js\",\n    \"chart.js\" => \"https://cdn.example.invalid/chart.js@REVIEWED_VERSION/+esm\"\n  },\n  preload: [\"javascript/app.js\"]\n) %>\n```\n\nThe example domain and version marker are intentionally nonfunctional. Replace\nthem only after reviewing a real provider, exact version, browser format,\nlicense, privacy impact, and outage behavior. Self-host the reviewed module\nunder `app/assets/javascript/vendor/` when the application must work without a\nthird-party runtime dependency.\n\n## CSS is part of the same release\n\nAn import map solves JavaScript names; it does not load styles. Keep CSS in the\nsame authored tree and resolve it through the same manifest.\n\n**File: `src/views/layouts/application.ecr` — place this helper in `<head>`.**\n\n```ecr\n<%= stylesheet_link_tag(\"stylesheets/app.css\") %>\n```\n\nThe compiler rewrites local `url(...)` references inside that stylesheet, so\nimages and fonts receive the same content-addressed release boundary. Do not\nappend hand-maintained `?v=` values to CSS, JavaScript, images, or fonts. A byte\nchange creates a new fingerprinted path automatically.\n\n## Build and verify\n\n**Run from: the application root.**\n\n```bash\namber assets build\namber assets check\ncrystal spec\namber watch\n```\n\nUse **View Source** and the browser network panel to confirm:\n\n1. exactly one import map appears before the module entry point;\n2. every local mapped value is a fingerprinted `/assets/` URL;\n3. every preloaded module is used and returns JavaScript;\n4. no source file imports a generated digest filename;\n5. there are no module-resolution or CSP errors; and\n6. rebuilding after a module edit changes its mapped URL.\n\nIf a strict manifest lookup fails, compare the logical value in\n`src/views/layouts/application.ecr` with the relative source path below\n`app/assets/`, then rebuild. Do not “fix” a missing entry by pasting a raw public\npath into the import map.\n\nContinue with [Stimulus integration](stimulus/) for an optional controller\norganization pattern."}