mcpField
M
maplibre-v6-migration

maplibre-v6-migration

github.com/maplibreDevelopment155198 installsUpdated today
Repository

About maplibre-v6-migration

A migration skill for MapLibre GL JS v5-to-v6 changes, for agents answering upgrade and debugging questions. It highlights seven breaking API and build changes that v5-era guidance may miss.

Key features

  1. 01Guidance for seven v5-to-v6 changes
  2. 02ESM import and CDN patterns
  3. 03Runtime image resolver migration
  4. 04Public camera API replacements
  5. 05Event type updates
  6. 06Bundler worker URL setup

Use cases

  1. 01Updating a MapLibre GL JS v5 app
  2. 02Debugging imports that fail in v6
  3. 03Fixing missing runtime sprite icons
  4. 04Resolving worker loading failures

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/maplibre/maplibre-agent-skills --skill maplibre-v6-migration

After installing, type/maplibre-v6-migrationin Claude Code to confirm it loaded.

Details

From the project README

MapLibre GL JS v5 → v6 Migration

Use this only when the question, or the code in it, involves one of the seven changes below. For any other MapLibre question, such as sources, layers, styling or terrain, write the answer with v6 imports and add no section, table, list or note about v6 changes or migrating from v5.

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 for code that touches one of these seven, the natural-sounding answer is usually the v5 one, 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

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".

<!-- ❌ v5 — dist/maplibre-gl.js no longer exists in v6 -->
<script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>

<!-- ✅ v6 — ESM build, type="module" required -->
<script type="module">
  import * as maplibregl from 'https://unpkg.com/maplibre-gl@^6.0.0/dist/maplibre-gl.mjs';
  const map = new maplibregl.Map({ container: 'map', style: '...' });
</script>

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 removed
import maplibregl from 'maplibre-gl';

// ✅ v6 — namespace import
import * as maplibregl from 'maplibre-gl';

// ✅ v6 — or import only what you use
import { 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.json exports 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 v6
const 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 dynamically
const { Map } = await import('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 that adds the image itself
map.setMissingStyleImageResolver(async (id) => {
  map.addImage(id, await generateIcon(id));
});

The resolver has to call map.addImage(). MapLibre ignores its return value, so returning the image does nothing.

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 transform
const { zoom, bearing, pitch } = map.transform;
const center = map.transform.center;

// ✅ v6 — public accessors
const 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 v6
import type { MapDataEvent } from 'maplibre-gl';
map.on('data', (e: MapDataEvent) => {
  /* ... */
});

// ✅ v6 — narrow on the union MapLibre now exports
import type { 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 renders
import { Map } from 'maplibre-gl';
const map = new Map({
  /* ... */
});

// ✅ Vite — the `?worker&url` query
import { 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 = new Map({
  /* ... */
});

// ✅ webpack 5+, Rspack, Rsbuild, and Next.js 15+ (Turbopack or webpack) — call setWorkerUrl() the same way, then create the Map
import { Map, setWorkerUrl } from 'maplibre-gl';

setWorkerUrl(new URL('maplibre-gl/dist/maplibre-gl-worker.mjs', import.meta.url).toString());

In Vite, use ?worker&url, as the install guide does. From 6.0.0 to 6.12.x, plain ?url leaves behind the maplibre-gl-shared.mjs the worker imports, so no vector tiles load in production; 6.13.0 made the worker self-contained. esbuild and Rollup copy the worker file and pass its URL to the same one-time call. See the install guide for current per-bundler snippets; they change 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().

Reference


This skill is a snapshot. Where a primary source contradicts it — the References above, MapLibre's current documentation, or what MapLibre does when you run it — that source wins. Follow it, then report the disagreement, citing the source and your MapLibre version: editing your installed copy helps no one else and is overwritten on the next update.

Comments

Markdown supported