diff --git a/Dockerfile b/Dockerfile
index 68c47f0d2..d425e8549 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,7 +22,6 @@ WORKDIR /app
COPY package.json package-lock.json ./
COPY shared/package.json ./shared/
COPY client/package.json ./client/
-COPY client/scripts/patch-maplibre.mjs ./client/scripts/
RUN npm ci --workspace=client
COPY --from=shared-builder /app/shared/dist ./shared/dist
COPY client/ ./client/
diff --git a/client/package.json b/client/package.json
index 12c0bf08e..4e38c0940 100644
--- a/client/package.json
+++ b/client/package.json
@@ -8,7 +8,7 @@
"type": "module",
"scripts": {
"dev": "vite",
- "prebuild": "node scripts/patch-maplibre.mjs && node scripts/generate-icons.mjs",
+ "prebuild": "node scripts/generate-icons.mjs",
"build": "vite build",
"build:analyze": "vite build --mode analyze",
"preview": "vite preview",
@@ -41,9 +41,7 @@
"shots:promote": "node e2e/screenshots/promote.mjs",
"e2e:report": "playwright show-report",
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.css\"",
- "format:check": "prettier --check \"src/**/*.tsx\" \"src/**/*.css\"",
- "postinstall": "node scripts/patch-maplibre.mjs",
- "predev": "node scripts/patch-maplibre.mjs"
+ "format:check": "prettier --check \"src/**/*.tsx\" \"src/**/*.css\""
},
"dependencies": {
"@assistant-ui/react": "0.15.25",
@@ -94,7 +92,7 @@
"leaflet": "^1.9.4",
"lucide-react": "^0.577.0",
"mapbox-gl": "^3.31.0",
- "maplibre-gl": "5.24.0",
+ "maplibre-gl": "6.15.0",
"modern-screenshot": "4.7.0",
"plyr": "^3.8.4",
"react": "^19.3.0",
diff --git a/client/scripts/format-baseline.json b/client/scripts/format-baseline.json
index 79098a567..5b54a7eeb 100644
--- a/client/scripts/format-baseline.json
+++ b/client/scripts/format-baseline.json
@@ -55,7 +55,6 @@
"scripts/check-gl-split.mjs",
"scripts/check-page-pattern.mjs",
"scripts/generate-icons.mjs",
- "scripts/patch-maplibre.mjs",
"src/App.test.tsx",
"src/App.tsx",
"src/App.versionHandover.test.tsx",
diff --git a/client/scripts/patch-maplibre.mjs b/client/scripts/patch-maplibre.mjs
deleted file mode 100644
index d9f99a89d..000000000
--- a/client/scripts/patch-maplibre.mjs
+++ /dev/null
@@ -1,34 +0,0 @@
-import { readFileSync, writeFileSync } from 'node:fs'
-import { createRequire } from 'node:module'
-import { dirname, join } from 'node:path'
-
-const require = createRequire(import.meta.url)
-const packagePath = require.resolve('maplibre-gl/package.json')
-const { version } = JSON.parse(readFileSync(packagePath, 'utf8'))
-
-if (version !== '5.24.0') throw new Error('Recheck the MapLibre sanitizer backport before changing its version')
-
-// Backport maplibre-gl-js#8189 without removing WebGL1 support in version 6.
-const loops = [
- ['of elem.attributes)', 'of Array.from(elem.attributes))'],
- ['of e.attributes)', 'of Array.from(e.attributes))'],
-]
-
-for (const filename of [
- 'src/util/dom.ts',
- 'dist/maplibre-gl.js',
- 'dist/maplibre-gl-dev.js',
- 'dist/maplibre-gl-csp.js',
- 'dist/maplibre-gl-csp-dev.js',
-]) {
- const target = join(dirname(packagePath), filename)
- const code = readFileSync(target, 'utf8')
- const loop = loops.find(([before, after]) => code.includes(before) || code.includes(after))
- if (!loop) throw new Error(`MapLibre sanitizer loop not found in ${filename}`)
- const [before, after] = loop
- if (code.split(before).length === 1 && code.split(after).length === 2) continue
- if (code.split(before).length !== 2 || code.includes(after)) {
- throw new Error(`Unexpected MapLibre sanitizer in ${filename}`)
- }
- writeFileSync(target, code.replace(before, after))
-}
diff --git a/client/src/components/Journey/JourneyMapGL.test.tsx b/client/src/components/Journey/JourneyMapGL.test.tsx
index c984f07bf..c7956ed60 100644
--- a/client/src/components/Journey/JourneyMapGL.test.tsx
+++ b/client/src/components/Journey/JourneyMapGL.test.tsx
@@ -107,7 +107,8 @@ const glModule = vi.hoisted(() => () => ({
}))
vi.mock('mapbox-gl', glModule)
-vi.mock('maplibre-gl', glModule)
+// maplibre-gl 6 has no default export: the same fakes, as named exports.
+vi.mock('maplibre-gl', () => glModule().default)
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}))
vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}))
@@ -121,7 +122,7 @@ vi.mock('../Map/mapboxSetup', () => ({
import React from 'react'
import mapboxgl from 'mapbox-gl'
-import maplibregl from 'maplibre-gl'
+import * as maplibregl from 'maplibre-gl'
import { render, screen, act } from '../../../tests/helpers/render'
import { resetAllStores, seedStore } from '../../../tests/helpers/store'
import { useSettingsStore } from '../../store/settingsStore'
diff --git a/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx b/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx
index 7eff8a397..0b183cd10 100644
--- a/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx
+++ b/client/src/components/Map/MapViewGL.plannerWaypoints.test.tsx
@@ -1,5 +1,5 @@
import { act } from '@testing-library/react';
-import maplibregl from 'maplibre-gl';
+import * as maplibregl from 'maplibre-gl';
import React from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render } from '../../../tests/helpers/render';
@@ -164,22 +164,21 @@ vi.mock('mapbox-gl', () => ({
}));
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}));
+// maplibre-gl 6 has no default export, so the fakes are named exports.
vi.mock('maplibre-gl', () => ({
- default: {
- Map: vi.fn(function () {
- return glMap;
- }),
- Marker: vi.fn(function (opts?: { element?: HTMLElement }) {
- return glMarkers.make(opts);
- }),
- LngLatBounds: vi.fn(function () {
- return glBounds.create();
- }),
- NavigationControl: vi.fn(),
- Popup: vi.fn(function () {
- return glPopup;
- }),
- },
+ Map: vi.fn(function () {
+ return glMap;
+ }),
+ Marker: vi.fn(function (opts?: { element?: HTMLElement }) {
+ return glMarkers.make(opts);
+ }),
+ LngLatBounds: vi.fn(function () {
+ return glBounds.create();
+ }),
+ NavigationControl: vi.fn(),
+ Popup: vi.fn(function () {
+ return glPopup;
+ }),
}));
vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}));
diff --git a/client/src/components/Map/MapViewGL.test.tsx b/client/src/components/Map/MapViewGL.test.tsx
index 6a1b4a0ac..ff0f01519 100644
--- a/client/src/components/Map/MapViewGL.test.tsx
+++ b/client/src/components/Map/MapViewGL.test.tsx
@@ -8,7 +8,7 @@ import { buildPlace } from '../../../tests/helpers/factories'
import { server } from '../../../tests/helpers/msw/server'
import { useSettingsStore } from '../../store/settingsStore'
import { useAuthStore } from '../../store/authStore'
-import maplibregl from 'maplibre-gl'
+import * as maplibregl from 'maplibre-gl'
import { DEFAULT_MAP_ZOOM } from '../../constants/mapDefaults'
import { MAP_LAYER_SWITCHER_INSET } from './MapLayerSwitcher'
import type { GeoPosition, TrackingMode } from '../../hooks/useGeolocation'
@@ -156,22 +156,21 @@ vi.mock('mapbox-gl', () => ({
}))
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}))
+// maplibre-gl 6 has no default export, so the fakes are named exports.
vi.mock('maplibre-gl', () => ({
- default: {
- Map: vi.fn(function () {
- return glMap
- }),
- Marker: vi.fn(function (opts?: { element?: HTMLElement }) {
- return glMarkers.make(opts)
- }),
- LngLatBounds: vi.fn(function () {
- return glBounds.create()
- }),
- NavigationControl: vi.fn(),
- Popup: vi.fn(function () {
- return glPopup
- }),
- },
+ Map: vi.fn(function () {
+ return glMap
+ }),
+ Marker: vi.fn(function (opts?: { element?: HTMLElement }) {
+ return glMarkers.make(opts)
+ }),
+ LngLatBounds: vi.fn(function () {
+ return glBounds.create()
+ }),
+ NavigationControl: vi.fn(),
+ Popup: vi.fn(function () {
+ return glPopup
+ }),
}))
vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}))
@@ -352,7 +351,7 @@ describe('MapViewGL', () => {
it('FE-COMP-MAPVIEWGL-004: renders with the MapLibre provider and no token', async () => {
const mapboxgl = (await import('mapbox-gl')).default
- const maplibregl = (await import('maplibre-gl')).default
+ const maplibregl = await import('maplibre-gl')
useSettingsStore.setState({
settings: {
...useSettingsStore.getState().settings,
@@ -373,7 +372,7 @@ describe('MapViewGL', () => {
it('FE-COMP-MAPVIEWGL-014: MapLibre maps disable the around-center mouse rotate (#1545)', async () => {
const mapboxgl = (await import('mapbox-gl')).default
- const maplibregl = (await import('maplibre-gl')).default
+ const maplibregl = await import('maplibre-gl')
useSettingsStore.setState({
settings: {
...useSettingsStore.getState().settings,
diff --git a/client/src/components/Map/engines/maplibre.ts b/client/src/components/Map/engines/maplibre.ts
index d67716a70..0c4a72f3a 100644
--- a/client/src/components/Map/engines/maplibre.ts
+++ b/client/src/components/Map/engines/maplibre.ts
@@ -1,7 +1,6 @@
-import maplibregl from 'maplibre-gl'
+import * as maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
-import rtlTextPluginUrl from '@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js?url'
-import { registerRtlTextPlugin } from '../rtlText'
+import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url'
/**
* The only module in the client that pulls maplibre-gl in at runtime. Same rule as
@@ -11,9 +10,22 @@ import { registerRtlTextPlugin } from '../rtlText'
* Popup, LngLatBounds — which is why the components take the engine as a prop and
* stay engine-agnostic. Where the two genuinely differ, the caller already guards
* on the provider (projection, accessToken, aroundCenter).
+ *
+ * MapLibre 6 ships as ES modules only and has no default export any more, so the
+ * namespace is what goes out as this module's default. It is a frozen module
+ * namespace: nothing may assign to it, which is why the accessToken writes stay
+ * behind their mapbox-only guard.
*/
-// Arabic/Hebrew/Persian labels come out unjoined and reversed without this.
-// MapLibre's signature is (url, lazy) and it resolves a promise — see rtlText.ts.
-registerRtlTextPlugin(() => maplibregl.setRTLTextPlugin(rtlTextPluginUrl, true))
+
+// MapLibre 6 finds its worker next to its own module, which inside a bundle is a
+// chunk in /assets/ with no worker beside it. Vite builds the worker into its own
+// file and hands back the URL; set before any Map is constructed, the
+// maplibre-gl-leaflet bridge included, since it only ever reaches the engine
+// together with this module.
+maplibregl.setWorkerUrl(workerUrl)
+
+// No RTL text plugin here, unlike mapbox. MapLibre 6.9 shapes Arabic and orders
+// bidirectional text itself; the old plugin would replace that implementation, and
+// a v6 worker can only load the plugin's UMD build through eval.
export default maplibregl
diff --git a/client/src/components/Map/engines/rtlRegistration.test.ts b/client/src/components/Map/engines/rtlRegistration.test.ts
index b2cfb1585..cb46e9c2f 100644
--- a/client/src/components/Map/engines/rtlRegistration.test.ts
+++ b/client/src/components/Map/engines/rtlRegistration.test.ts
@@ -1,29 +1,33 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
/**
- * The two lines this fix actually adds. Everything else is a helper: the guard in
- * rtlText.ts has its own tests, but a guard nobody calls looks exactly like a
- * working fix from the outside. Deleting either registration, swapping an
- * argument or dropping the deferred flag left the whole suite green.
+ * What each engine module does on import. Everything else is a helper: the guard
+ * in rtlText.ts has its own tests, but a guard nobody calls looks exactly like a
+ * working fix from the outside. Deleting the mapbox registration, swapping an
+ * argument or dropping the deferred flag left the whole suite green, and so would
+ * a maplibre engine that forgot its worker URL.
*
* The engines are mocked because importing them for real builds a worker pool
* under jsdom, and the plugin URL is mocked so the 133 KB payload never loads.
*/
-const { mapboxSet, maplibreSet } = vi.hoisted(() => ({
+const { mapboxSet, maplibreSet, maplibreWorker } = vi.hoisted(() => ({
mapboxSet: vi.fn(),
maplibreSet: vi.fn(),
+ maplibreWorker: vi.fn(),
}));
vi.mock('mapbox-gl', () => ({ default: { setRTLTextPlugin: mapboxSet } }));
-vi.mock('maplibre-gl', () => ({ default: { setRTLTextPlugin: maplibreSet } }));
+vi.mock('maplibre-gl', () => ({ setRTLTextPlugin: maplibreSet, setWorkerUrl: maplibreWorker }));
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}));
vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}));
+vi.mock('maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url', () => ({ default: '/assets/maplibre-worker.js' }));
vi.mock('@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js?url', () => ({ default: '/assets/rtl-plugin.js' }));
beforeEach(() => {
vi.resetModules();
mapboxSet.mockReset();
maplibreSet.mockReset();
+ maplibreWorker.mockReset();
});
describe('the GL engines register the RTL text plugin (#2235)', () => {
@@ -37,13 +41,12 @@ describe('the GL engines register the RTL text plugin (#2235)', () => {
expect(mapboxSet).toHaveBeenCalledWith(expect.stringContaining('rtl'), null, true);
});
- it('FE-MAP-RTL-002: importing the maplibre engine registers it lazily', async () => {
+ it('FE-MAP-RTL-002: importing the maplibre engine registers no plugin', async () => {
await import('./maplibre');
- expect(maplibreSet).toHaveBeenCalledTimes(1);
- // MapLibre 5 takes (url, lazy) and returns a promise, which is the reason
- // the call stays with each engine instead of going through one wrapper.
- expect(maplibreSet).toHaveBeenCalledWith(expect.stringContaining('rtl'), true);
+ // MapLibre 6.9 shapes right-to-left text itself. The plugin would replace
+ // that, and a v6 worker could only run its UMD build through eval.
+ expect(maplibreSet).not.toHaveBeenCalled();
});
it('FE-MAP-RTL-003: an engine still loads when its registration throws', async () => {
@@ -54,13 +57,13 @@ describe('the GL engines register the RTL text plugin (#2235)', () => {
expect(engine.default).toBeDefined();
});
- it('FE-MAP-RTL-004: a rejected registration promise does not surface as an unhandled rejection', async () => {
- maplibreSet.mockReturnValue(Promise.reject(new Error('already registered')));
-
+ it('FE-MAP-RTL-004: importing the maplibre engine points it at the bundled worker', async () => {
const engine = await import('./maplibre');
- expect(engine.default).toBeDefined();
- // Give the rejection a tick to go unhandled if nothing caught it.
- await new Promise((resolve) => setTimeout(resolve, 0));
+ // MapLibre 6 looks for its worker next to its own module, which in a bundle
+ // is a chunk with no worker beside it. Without this no map draws a tile.
+ expect(maplibreWorker).toHaveBeenCalledTimes(1);
+ expect(maplibreWorker).toHaveBeenCalledWith('/assets/maplibre-worker.js');
+ expect(engine.default.setWorkerUrl).toBe(maplibreWorker);
});
});
diff --git a/client/src/components/Map/rtlText.ts b/client/src/components/Map/rtlText.ts
index c260772ec..340922b9a 100644
--- a/client/src/components/Map/rtlText.ts
+++ b/client/src/components/Map/rtlText.ts
@@ -7,16 +7,18 @@
* this because their labels are drawn server-side, which is why it only surfaced
* once the default basemap became a vector style.
*
- * Both engines expose the fix as an optional plugin and neither loads it by
- * default. Registering is global to the engine, so it happens once per engine
- * module rather than at each map's construction site.
+ * Mapbox GL exposes the fix as an optional plugin and does not load it by
+ * default. Registering is global to the engine, so it happens once in the engine
+ * module rather than at each map's construction site. MapLibre needed the same
+ * plugin up to version 5; since 6.9 it shapes and reorders the text itself, and
+ * its engine module registers nothing.
*
- * The call itself stays with the caller: this is one of the places the two engines
- * genuinely differ. MapLibre 5 takes `(url, lazy)` and returns a promise, Mapbox
- * GL 3 takes `(url, callback, deferred)` and returns nothing — so each engine
- * module passes its own invocation rather than a shared wrapper guessing an arity.
+ * The call itself stays with the caller: Mapbox GL 3 takes
+ * `(url, callback, deferred)` and returns nothing, while MapLibre took
+ * `(url, lazy)` and returned a promise, so the guard below accepts either rather
+ * than guessing an arity.
*
- * Deliberately no getRTLTextPluginStatus() pre-check: reading it makes MapLibre
+ * Deliberately no getRTLTextPluginStatus() pre-check: reading it made MapLibre
* build its worker pool, which is an eager side effect for a module that is
* imported for its default export, and throws outright under jsdom. Registering
* twice is reported — by a throw or a rejected promise depending on the engine —
diff --git a/client/src/components/Settings/MapboxPreview.test.tsx b/client/src/components/Settings/MapboxPreview.test.tsx
index b028225e6..1b06aa1f1 100644
--- a/client/src/components/Settings/MapboxPreview.test.tsx
+++ b/client/src/components/Settings/MapboxPreview.test.tsx
@@ -32,7 +32,7 @@ const mapboxModule = vi.hoisted(() => ({ accessToken: '', Map: mapboxCtor }));
vi.mock('mapbox-gl', () => ({ default: mapboxModule }));
vi.mock('mapbox-gl/dist/mapbox-gl.css', () => ({}));
-vi.mock('maplibre-gl', () => ({ default: { Map: maplibreCtor } }));
+vi.mock('maplibre-gl', () => ({ Map: maplibreCtor }));
vi.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}));
vi.mock('../Map/mapboxSetup', () => ({
@@ -45,7 +45,7 @@ vi.mock('../Map/mapboxSetup', () => ({
import { isStandardFamily, supportsCustom3d, addCustom3dBuildings, addTerrainAndSky } from '../Map/mapboxSetup';
import { MAPBOX_DEFAULT_STYLE, OPENFREEMAP_DEFAULT_STYLE } from '../Map/glProviders';
import mapboxgl from 'mapbox-gl';
-import maplibregl from 'maplibre-gl';
+import * as maplibregl from 'maplibre-gl';
import GlMapPreviewWithEngine from './MapboxPreview';
// The engine is a prop now, not a module import — that is what keeps mapbox-gl and
diff --git a/client/src/utils/webgl.test.ts b/client/src/utils/webgl.test.ts
index c69cecce6..fa8422428 100644
--- a/client/src/utils/webgl.test.ts
+++ b/client/src/utils/webgl.test.ts
@@ -42,15 +42,15 @@ describe('hasWebGL', () => {
expect(hasWebGL()).toBe(false)
})
- it('FE-UTIL-WEBGL-003: falls back to webgl1 when only webgl2 is refused', () => {
- // Exactly the order maplibre-gl asks in. A stricter probe would black out
- // maps that would in fact have drawn.
+ it('FE-UTIL-WEBGL-003: says no when only webgl1 is on offer', () => {
+ // MapLibre 6 asks for webgl2 and nothing else. A probe that settled for
+ // webgl1 would wave the map through to a GPUInitializationError.
const { ctx } = fakeContext()
const getContext = vi.fn((type: string) => (type === 'webgl2' ? null : ctx))
HTMLCanvasElement.prototype.getContext = getContext as unknown as typeof realGetContext
- expect(hasWebGL()).toBe(true)
- expect(getContext.mock.calls.map((c) => c[0])).toEqual(['webgl2', 'webgl'])
+ expect(hasWebGL()).toBe(false)
+ expect(getContext.mock.calls.map((c) => c[0])).toEqual(['webgl2'])
})
it('FE-UTIL-WEBGL-004: gives the probe context straight back', () => {
diff --git a/client/src/utils/webgl.ts b/client/src/utils/webgl.ts
index 92a0be3b1..5c1e28362 100644
--- a/client/src/utils/webgl.ts
+++ b/client/src/utils/webgl.ts
@@ -1,5 +1,5 @@
/**
- * Whether this browser will hand MapLibre a WebGL context.
+ * Whether this browser will hand MapLibre a WebGL2 context.
*
* The default basemap has been a vector style since 4.1.0, and maplibre-gl builds
* its GL context inside the Map constructor: no context, no map, and the throw
@@ -45,12 +45,13 @@ function probe(): boolean {
canvas.width = 1
canvas.height = 1
- // Exactly the order maplibre-gl asks in (Map._setupPainter): webgl2 first,
- // webgl1 second. A stricter probe would black out maps that would in fact
- // have drawn; a looser one would let the throw through anyway.
- const gl = (canvas.getContext('webgl2') ?? canvas.getContext('webgl')) as WebGLRenderingContext | null
+ // Exactly what maplibre-gl asks for (Map._setupPainter): WebGL2 and nothing
+ // else, since MapLibre 6 dropped WebGL1. A browser that only offers WebGL1
+ // passes a looser probe and then throws GPUInitializationError out of the Map
+ // constructor, which is the crash this probe exists to prevent.
+ const gl = canvas.getContext('webgl2')
if (!gl) {
- console.warn('[basemap] this browser refuses a WebGL context, vector basemaps are unavailable')
+ console.warn('[basemap] this browser refuses a WebGL2 context, vector basemaps are unavailable')
return false
}
diff --git a/client/tests/unit/maplibre-sanitizer.test.ts b/client/tests/unit/maplibre-sanitizer.test.ts
index 2705d05df..0a9d51566 100644
--- a/client/tests/unit/maplibre-sanitizer.test.ts
+++ b/client/tests/unit/maplibre-sanitizer.test.ts
@@ -1,8 +1,14 @@
import { describe, expect, it } from 'vitest'
-import maplibregl from 'maplibre-gl'
+import { AttributionControl, type Map } from 'maplibre-gl'
+/**
+ * GHSA-jrc7-96c5-q579: MapLibre's attribution sanitizer let a second dangerous
+ * attribute through after removing the first, so a tile source's attribution
+ * could run script. TREK carried a patch for this on 5.x; 6.x replaced the
+ * sanitizer with an allow list, and these cases hold the upgrade to it.
+ */
function attribution(html: string, fromSource = false) {
- const control = new maplibregl.AttributionControl({
+ const control = new AttributionControl({
compact: false,
customAttribution: fromSource ? undefined : html,
})
@@ -16,17 +22,17 @@ function attribution(html: string, fromSource = false) {
on: () => {},
off: () => {},
getCanvasContainer: () => document.createElement('div'),
- } as unknown as maplibregl.Map
+ } as unknown as Map
return control.onAdd(map)
}
-describe('MapLibre attribution sanitizer backport', () => {
- it.each([false, true])('removes adjacent event handlers (source: %s)', fromSource => {
+describe('MapLibre attribution sanitizer', () => {
+ it.each([false, true])('leaves no event handler behind (source: %s)', fromSource => {
const container = attribution('Credit ', fromSource)
- const details = container.querySelector('.maplibregl-ctrl-attrib-inner details')!
- expect(details.hasAttribute('onload')).toBe(false)
- expect(details.hasAttribute('ontoggle')).toBe(false)
- expect(details.textContent).toBe('Credit')
+ const inner = container.querySelector('.maplibregl-ctrl-attrib-inner')!
+ // is not on the allow list, so it goes with its subtree.
+ expect(inner.querySelector('details')).toBeNull()
+ expect(inner.querySelectorAll('[onload], [ontoggle]')).toHaveLength(0)
})
it('removes consecutive dangerous URLs and handlers in nested markup', () => {
diff --git a/package-lock.json b/package-lock.json
index 43d5e139a..182299b1a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,6 @@
"client": {
"name": "@trek/client",
"version": "4.3.4",
- "hasInstallScript": true,
"dependencies": {
"@assistant-ui/react": "0.15.25",
"@fontsource/abril-fatface": "^5.3.0",
@@ -80,7 +79,7 @@
"leaflet": "^1.9.4",
"lucide-react": "^0.577.0",
"mapbox-gl": "^3.31.0",
- "maplibre-gl": "5.24.0",
+ "maplibre-gl": "6.15.0",
"modern-screenshot": "4.7.0",
"plyr": "^3.8.4",
"react": "^19.3.0",
@@ -4971,44 +4970,35 @@
"license": "ISC"
},
"node_modules/@mapbox/tiny-sdf": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
- "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.3.0.tgz",
+ "integrity": "sha512-gBvcNDBBB66bgQYvnaJ6Jk+VCaMmVq5+IZR2kWPisT88Z8s0kBQt6hfVi8QgRW8Vpg0UWZzBA4ZSyzHHC+VeRA==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/unitbezier": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
- "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz",
+ "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/vector-tile": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.5.tgz",
- "integrity": "sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz",
+ "integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/point-geometry": "~1.1.0",
"@types/geojson": "^7946.0.16",
- "pbf": "^4.0.2"
- }
- },
- "node_modules/@mapbox/whoots-js": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
- "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
- "license": "ISC",
- "engines": {
- "node": ">=6.0.0"
+ "pbf": "^5.0.0"
}
},
"node_modules/@maplibre/geojson-vt": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz",
- "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.3.0.tgz",
+ "integrity": "sha512-3Zw2jsU9pIJG3G2CQFbLjWj2MlAHodhD7cSY+15iywUuLmmkgzcKRnueVRZ4AcC/5wOtfhidcJAAzBpoBNs3Xg==",
"license": "ISC",
"dependencies": {
- "kdbush": "^4.0.2"
+ "kdbush": "^4.1.0"
}
},
"node_modules/@maplibre/maplibre-gl-leaflet": {
@@ -5023,12 +5013,12 @@
}
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
- "version": "24.10.0",
- "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.10.0.tgz",
- "integrity": "sha512-lichxSiagMEBBrqHF0trtMQH9RKh+9jUlIJl0qW0QHvt2H/tbvUWdE+ZzI2Jd0/pT7j/iavLonlPu7EQ/ixTOw==",
+ "version": "26.4.4",
+ "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.4.tgz",
+ "integrity": "sha512-hG4RZR4PBd/+6A3ODdZ3AnQRF106ZQk1y3bhwDg8bH/A4Oz2tnRUJVVWs9BZa6gXZ1d3UKe89C+Mm+olGBELvA==",
"license": "ISC",
"dependencies": {
- "@mapbox/jsonlint-lines-primitives": "~2.0.2",
+ "@mapbox/jsonlint-lines-primitives": "^2.0.3",
"@mapbox/unitbezier": "^1.0.0",
"json-stringify-pretty-compact": "^4.0.0",
"minimist": "^1.2.8",
@@ -5041,16 +5031,10 @@
"gl-style-validate": "dist/gl-style-validate.mjs"
}
},
- "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz",
- "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==",
- "license": "BSD-2-Clause"
- },
"node_modules/@maplibre/mlt": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.12.tgz",
- "integrity": "sha512-ZeK5w2TTeHOajcLaEQs1KZXw2V9wIKo1PmThlxlsHoXsQsYlBqLJzPOd6tJHRtGTChUY3DPPmjXRArYVvAbmZw==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.3.0.tgz",
+ "integrity": "sha512-7M2O7ABM80Oi8XPkfkrjKB8mOBIxGoVgkA52Mu4RMWoBvvMdagQ522+WxVYujZsMpqIr3BpzrTbXrm4k8lCzCQ==",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
"@mapbox/point-geometry": "^1.1.0"
@@ -5067,18 +5051,6 @@
"pbf": "^5.1.0"
}
},
- "node_modules/@maplibre/vt-pbf/node_modules/pbf": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.0.tgz",
- "integrity": "sha512-Wv0yo0+uZepnoNEKsquhar1F18LogB8oeEikIhUXG16udbiXG7JecHGySwoo6kuMgjmbQYzdrTZlO+/K9t8eZg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "resolve-protobuf-schema": "^2.1.0"
- },
- "bin": {
- "pbf": "bin/pbf"
- }
- },
"node_modules/@microsoft/tsdoc": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz",
@@ -13603,9 +13575,9 @@
}
},
"node_modules/earcut": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
- "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.4.tgz",
+ "integrity": "sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==",
"license": "ISC"
},
"node_modules/ecdsa-sig-formatter": {
@@ -17490,27 +17462,26 @@
]
},
"node_modules/maplibre-gl": {
- "version": "5.24.0",
- "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz",
- "integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.15.0.tgz",
+ "integrity": "sha512-kxFetbvUHDOVaAP1CYb226Mdk2QhzIPddqM0xJTFD35wu78Mpd6NM7+H68ALjc6Oa0kAOX5EAdMKyvD2u3qfcg==",
"license": "BSD-3-Clause",
"dependencies": {
- "@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/point-geometry": "^1.1.0",
- "@mapbox/tiny-sdf": "^2.1.0",
- "@mapbox/unitbezier": "^0.0.1",
- "@mapbox/vector-tile": "^2.0.4",
- "@mapbox/whoots-js": "^3.1.0",
- "@maplibre/geojson-vt": "^6.1.0",
- "@maplibre/maplibre-gl-style-spec": "^24.8.1",
- "@maplibre/mlt": "^1.1.8",
- "@maplibre/vt-pbf": "^4.3.0",
+ "@mapbox/tiny-sdf": "^2.3.0",
+ "@mapbox/unitbezier": "^1.0.0",
+ "@mapbox/vector-tile": "^3.0.0",
+ "@maplibre/geojson-vt": "^6.3.0",
+ "@maplibre/maplibre-gl-style-spec": "^26.4.4",
+ "@maplibre/mlt": "^1.3.0",
+ "@maplibre/vt-pbf": "^4.3.2",
"@types/geojson": "^7946.0.16",
- "earcut": "^3.0.2",
+ "bidi-js": "^1.1.0",
+ "earcut": "^3.2.4",
"gl-matrix": "^3.4.4",
- "kdbush": "^4.0.2",
+ "kdbush": "^4.1.0",
"murmurhash-js": "^1.0.0",
- "pbf": "^4.0.1",
+ "pbf": "^5.1.2",
"potpack": "^2.1.0",
"quickselect": "^3.0.0",
"tinyqueue": "^3.0.0"
@@ -19398,9 +19369,9 @@
"license": "MIT"
},
"node_modules/pbf": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.2.tgz",
- "integrity": "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz",
+ "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==",
"license": "BSD-3-Clause",
"dependencies": {
"resolve-protobuf-schema": "^2.1.0"