Load JavaScript modules without bundlers Define pins in Crystal, merge them per‑namespace, and render a
<script type="importmap">tag at runtime.
- Crystal DSL – declare pins and namespaces in pure Crystal code.
- Namespaced import maps – serve different JS sets for public, admin, etc.
- Module‑preload links – automatic
<link rel="modulepreload">for every pin (opt‑out supported). - Pluggable resolver – hook in your asset pipeline to rewrite
/js/app.js→/assets/app‑abc123.js. - Zero runtime overhead – import‑map JSON is cached on boot.
Add the shard to your shard.yml and run shards install.
dependencies:
importmap:
github: treagod/importmap
version: "~> 0.2.0"- Configure pins
require "importmap"
ImportMap.draw do
# Base pins (preload = true by default)
pin "stimulus", "/js/stimulus.js"
namespace "admin" do
pin "admin-ui", "/js/admin.js", preload: false # opt‑out
end
end- (Optional) integrate a resolver
ImportMap.resolver = ->(path : String) { "/assets#{path}?v=abc" }- Render the tags
ImportMap.tag
# admin namespace with an custom entrypoint
ImportMap.tag("admin", entrypoint: "admin-ui")Result:
<link rel="modulepreload" href="/js/stimulus.js">
<script type="importmap">{"imports":{"stimulus":"/js/stimulus.js"}}</script>
<!-- admin namespace -->
<link rel="modulepreload" href="/js/stimulus.js">
<script type="importmap" data-namespace="admin">{"imports":{"stimulus":"/js/stimulus.js","admin-ui":"/js/admin.js"}}</script>
<script type="module">import "admin-ui"</script>Use pin_all_from to automatically expose every .js/.mjs file in a directory under a namespace. This mirrors Rails' importmap ergonomics for Stimulus controllers or other entrypoints.
ImportMap.draw do
pin "@hotwired/stimulus", to: "vendor/stimulus.js"
pin_all_from "assets/controllers", under: "controllers", to: "controllers"
endunder:controls the import specifier namespace (controllers/menu_controllerin the example above).to:controls the logical asset path that gets passed to your resolver. Omit it to use the same prefix asunder(or the raw relative path ifunderisnil).preload:defaults totrue, matchingpin.
pin_all_from entries participate in the same caching as regular pins, so calling ImportMap.draw (or ImportMap::Manager.instance.pin_all_from) followed by ImportMap.tag keeps runtime overhead low.
Because the directory is only scanned when the import map is rendered, files added or removed afterwards are not picked up by the cached output. Call ImportMap.invalidate_cache! (or ImportMap::Manager.instance.invalidate_cache!) to drop the cached JSON and preload URLs — pins, namespaces and the resolver are preserved:
ImportMap.invalidate_cache!- CLI tool for pinning/unpinning CDN packages
- Static build command (importmap build)
- Vulnerability & outdated‑package audit
- Add importmap scopes
- Fork, create a feature branch.
shards install && crystal spec(all tests should pass).- Open a PR!