tonysm/importmap-laravel
Importmap Laravel brings native ES module import maps to Laravel, letting you use modern JavaScript packages without a bundler. Pin dependencies from CDNs, manage vendor modules, and skip complex build steps while keeping Blade and Laravel integration simple.
composer require tonysm/importmap-laravel
php artisan importmap:install
The installer scaffolds three things you should open immediately, in this order:
config/importmap.php — the single source of truth. Entries are "pins": either a path into your own tree, a url to a CDN, or a package downloaded into vendor/javascript. Each pin can carry preload and download flags.resources/js/application.js — the default entry point. This is plain ESM, imported directly by the browser.<head>
@importmap {{-- <script type="importmap"> — must precede every module --}}
@importmap_entry_point {{-- <script type="module" src="...application.js"> --}}
</head>
First use case: pin a CDN library and consume it with a bare specifier.
php artisan importmap:pin chart.js --preload
// resources/js/application.js
import Chart from "chart.js"
new Chart(document.querySelector("canvas"), { /* ... */ })
Refresh the page. No npm run dev, no watcher, no manifest. That absence of a loop is the feature — your mental model shifts from "build output" to "URLs the browser fetches."
If you're unsure of a command's options, php artisan importmap:pin --help, importmap:update --help, and importmap:outdated are the fastest way to learn the surface.
CDN pins for speed, downloaded pins for stability. importmap:pin foo resolves the dependency graph via jspm.io and writes transitive pins into config/importmap.php. Add --download to vendor the files into vendor/javascript and pin them locally — commit that directory. A pragmatic split: CDN with preload for public marketing pages, downloaded pins for anything on a critical path or behind a compliance requirement.
Local modules by relative path, shared code by directory pins. Anything under resources/js is reachable as import "./components/modal.js". When sibling modules need a stable name (e.g. import { formatMoney } from "utils"), pin the directory instead of littering the map:
'utils' => ['path' => 'resources/js/utils/index.js', 'preload' => true],
Multiple entry points per layout. The entry point is just a pin. Keep application.js for the public site and add admin.js as a second entry point rendered only in the admin layout — one import map, several <script type="module"> tags. Don't try to share one entry point across layouts with conditionals inside JS.
Programmatic pinning in a service provider. For packages that must be pinned identically across many apps (an internal design system, say), pin in AppServiceProvider::boot() with the facade rather than hand-editing config. This keeps the config file diffable and lets you derive URLs from an env var.
Preload what blocks first paint. @importmap_preload emits <link rel="modulepreload"> for flagged pins. Mark the two or three modules your entry point imports synchronously; over-preloading is worse than none because it competes with your CSS.
Hotwire/Stimulus pairing. This package is the intended sibling of tonysm/turbo-laravel and the Stimulus controllers package. The idiom is: controllers registered by eager-importing a controllers/ directory, Turbo driving navigation, and the import map only ever changing when you add a library. If you're adopting Turbo, adopt all three at once.
CI guardrails. Run importmap:audit in CI — it fails when code imports a bare specifier that isn't pinned, which is otherwise a silent runtime TypeError in the browser that never shows up in PHP tests. Pair with importmap:outdated on a schedule; importmap:update rewrites pins in place.
CSS is not this package's job. Keep Vite (or the Tailwind CLI) for stylesheets and let it emit one static CSS file. Trying to route CSS through the import map is a dead end — import maps are for JS modules only.
@importmap in the body after a <script type="module"> silently loses. Also avoid putting it behind a deferred partial or a Turbo frame.@importmap emits an inline <script type="importmap">, so a strict script-src without 'unsafe-inline' will block it. Confirm how the package's nonce hooks interact with your CSP package before shipping; this is the most common first-deploy breakage.config/importmap.php. In a deploy that runs config:cache, the new pins won't take effect until the cache is rebuilt — put php artisan config:cache after any pin-changing step, and never run importmap:* at runtime in prod.import of extensionless relative files — browsers need the literal .js in the URL. Code that worked under webpack will 404 here.url pin does not include its dependencies; you must pin those too or the browser throws an unresolved-specifier error. importmap:pin handles this because jspm resolves the whole graph.import "lodash" and import "lodash/merge" are two different map entries. Audit catches the missing one.path pins. Backslashes produce URLs the browser won't resolve and the error message is unhelpful.importmap:outdated to catch it.<script type="importmap"> JSON directly. Nine out of ten "module not found" issues are visible as a missing or misnamed key in that blob, not in your JS.Importmap facade is the seam — wrap it in your own service or Blade component if you need tenant-specific or feature-flagged pin sets, rather than branching inside config/importmap.php.How can I help you explore Laravel packages today?