Upgrading a MapLibre GL JS app from v5 to v6 — the ESM-only build, the removed default export, CommonJS require() breakage, the styleimagemissing/setMissingStyleImageResolver change, the removal of the internal map.transform, the MapDataEvent → MapSourceDataEvent/MapStyleDataEvent split, and the bundler-only setWorkerUrl() requirement. Use when a v5 app breaks after upgrading to v6, or before pinning a v6 install.
Upgrading a MapLibre GL JS app from v5 to v6 — the ESM-only build, the removed default export, CommonJS require() breakage, the styleimagemissing/setMissingStyleImageResolver change, the removal of the internal map.transform, the MapDataEvent → MapSourceDataEvent/MapStyleDataEvent split, and the bundler-only setWorkerUrl() requirement. Use when a v5 app breaks after upgrading to v6, or before pinning a v6 install.
status
verified
MapLibre GL JS v5 → v6 Migration
MapLibre GL JS v6 (released 2026-07-22) removed several things v5 code relied on: the UMD/CSP browser bundles, the default export, CommonJS support, the internal map.transform, and the MapDataEvent type. Most models' training data predates v6. So the natural-sounding answer to "how do I do X" is usually the v5 answer, and it breaks on v6.
Primary reference: the MapLibre GL JS v5→v6 migration guide. This skill adds the gaps a model tends to fill with v5-era defaults. If the guide and this skill disagree, follow the guide and report it.
When to Use This Skill
Upgrading an existing MapLibre GL JS v5 app to v6, or debugging a map that "used to work" after a dependency update
Writing one of the seven patterns below: a CDN <script> tag, an import statement, a require() call, a styleimagemissing handler, code that reads map.transform, a typed data/dataloading/dataabort handler, or a bundler setup (Vite, webpack, esbuild, Rspack, Rollup)
Debugging errors like ERR_PACKAGE_PATH_NOT_EXPORTED, a blank map after a CDN update, a sprite icon that never appears, a TypeScript error naming MapDataEvent, or a worker-loading error that blocks render
Don't use this skill to pad an unrelated answer. It covers seven narrow breaking changes, not general v6 best practice. A question about sources, layers, styling, or terrain gets a normal, focused answer with no migration reminders attached.
1. CDN script tag: ESM-only now
v6 removed the UMD bundle and the separate CSP build. There's no dist/maplibre-gl.js for a plain <script src="..."> tag anymore. Only the ESM build, dist/maplibre-gl.mjs, remains, and it needs type="module".
Always pin an explicit major (@^6.0.0, or a specific version). Never use @latest or a bare unversioned specifier — an unpinned CDN URL breaks the page silently the moment the next major version publishes. This already happened at the v5→v6 boundary.
2. No default export — named or namespace import only
v5's import maplibregl from 'maplibre-gl' (default import) no longer works in v6.
// ❌ v5 — default export removedimport maplibregl from'maplibre-gl';
// ✅ v6 — namespace importimport * as maplibregl from'maplibre-gl';
// ✅ v6 — or import only what you useimport { Map, NavigationControl } from'maplibre-gl';
This applies whether the code is new or converted from a Mapbox GL JS snippet (import mapboxgl from 'mapbox-gl'). Don't carry the default-import shape over either way.
3. No CommonJS — require('maplibre-gl') throws
maplibre-gl's package.jsonexports field has only an "import" condition in v6, not "require". So require('maplibre-gl') throws ERR_PACKAGE_PATH_NOT_EXPORTED in plain Node. That's expected behavior, not a bug in the caller's setup.
// ❌ throws ERR_PACKAGE_PATH_NOT_EXPORTED in v6const maplibregl = require('maplibre-gl');
// ✅ convert the file to ESM (named/namespace import — see item 2)import { Map } from'maplibre-gl';
// ✅ or, if the caller must stay CommonJS, load it dynamicallyconst { Map } = awaitimport('maplibre-gl');
This section is only about a bare Node require() call. If a bundler is involved (webpack, Vite, etc.) and require() still fails, that's a separate ESM-interop config issue.
4. styleimagemissing no longer resolves the request — use setMissingStyleImageResolver
In v5, listening for styleimagemissing and calling map.addImage() synchronously from the handler would supply the missing icon. In v6, styleimagemissing is notify-only. Calling addImage from the handler no longer resolves the pending request.
// ❌ v5 pattern — no longer resolves the request in v6
map.on('styleimagemissing', (e) => {
map.addImage(e.id, generateIcon(e.id));
});
// ✅ v6 — register a resolver
map.setMissingStyleImageResolver((id) => {
returngenerateIcon(id); // or return a Promise
});
Use this whenever a style references icon-image names that aren't in the sprite sheet and need to be generated or fetched at runtime.
5. map.transform is gone — use the public Camera API
v6 refactored Map to compose a Camera instead of extending it. Map now extends Evented directly and forwards the camera API. The internal map.transform property was removed as part of that change.
// ❌ v5 — reaching into the internal transformconst { zoom, bearing, pitch } = map.transform;
const center = map.transform.center;
// ✅ v6 — public accessorsconst zoom = map.getZoom();
const bearing = map.getBearing();
const pitch = map.getPitch();
const center = map.getCenter();
There's no general public replacement for the raw projection/view matrix. It was never exposed on Map itself, in v5 or v6 — it only ever lived on map.transform. The one place a matrix is still available is inside a custom layer's render() callback, via the callback's arguments (CustomRenderMethodInput.getProjectionData() / defaultProjectionData; see the custom layers API). Outside a custom layer, use the public getters above. For anything else the public API doesn't expose, open an issue or PR instead of reintroducing a private accessor.
6. MapDataEvent removed — use MapSourceDataEvent / MapStyleDataEvent
v6 made every fired event a real class, instantiated per event. The old catch-all MapDataEvent type is gone. data, dataloading, and dataabort are now typed as MapSourceDataEvent | MapStyleDataEvent. A source data event carries its full source info (sourceId, tile, sourceDataType, ...) directly on its own type, instead of a generic shared shape.
// ❌ v5 — MapDataEvent no longer exists in v6importtype { MapDataEvent } from'maplibre-gl';
map.on('data', (e: MapDataEvent) => {
/* ... */
});
// ✅ v6 — narrow on the union MapLibre now exportsimporttype { MapSourceDataEvent, MapStyleDataEvent } from'maplibre-gl';
map.on('data', (e: MapSourceDataEvent | MapStyleDataEvent) => {
if ('sourceId'in e) {
// MapSourceDataEvent — e.sourceId, e.sourceDataType, e.tile
} else {
// MapStyleDataEvent
}
});
The same fix applies anywhere a type import or annotation names MapDataEvent, including dataloading and dataabort handlers.
7. Bundled builds still need setWorkerUrl() — CDN ESM does not
v6 changed how the source-processing worker is located, but only for bundled apps. A plain CDN <script type="module"> (item 1) auto-detects the worker URL from import.meta.url and needs no extra setup. Inside a bundler (Vite, webpack, esbuild, Rspack, Rsbuild, Rollup), import.meta.url doesn't reliably resolve to the worker file within the bundler's own module graph. Each of these setups needs one explicit setWorkerUrl() call, made once before creating the Map.
// ❌ v6 with a bundler, no setWorkerUrl() call — worker fails to load, map never rendersimport { Map } from'maplibre-gl';
const map = newMap({
/* ... */
});
// ✅ Vite — the `?worker&url` query bundles the worker's own dependencies with itimport { Map, setWorkerUrl } from'maplibre-gl';
import'maplibre-gl/dist/maplibre-gl.css';
import workerUrl from'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
setWorkerUrl(workerUrl);
const map = newMap({
/* ... */
});
// ✅ Webpack 5+ (Rspack and Rsbuild use the same pattern) — call setWorkerUrl() the same way, then create the Mapimport { Map, setWorkerUrl } from'maplibre-gl';
setWorkerUrl(newURL('maplibre-gl/dist/maplibre-gl-worker.mjs', import.meta.url).toString());
In Vite, use ?worker&url, not plain ?url. The worker file imports a sibling maplibre-gl-shared.mjs, and plain ?url emits the worker verbatim without it. That makes the worker fail on its first import in production, so no vector tiles load. Esbuild, Rollup, and Turbopack need the same one-time call with their own asset-handling syntax; Next.js needs a different approach entirely. See the install guide for current per-bundler snippets — this changes with tooling versions faster than the rest of this skill.
Don't carry this into item 1's CDN case. A plain <script type="module"> import from a CDN URL never needs setWorkerUrl().