chore(deps): maplibre-gl 5 + vue-maplibre-gl 5 + @maptiler/* 3#581
Closed
JumpLink wants to merge 18 commits into
Closed
chore(deps): maplibre-gl 5 + vue-maplibre-gl 5 + @maptiler/* 3#581JumpLink wants to merge 18 commits into
JumpLink wants to merge 18 commits into
Conversation
4 tasks
…2 → 3 API changes that reach our code: - maplibre-gl 5 turned `Map.loadImage` and `GeoJSONSource.getClusterExpansionZoom` into Promise-returning methods. Updated `apps/frontend/src/utils/images.ts` and `apps/frontend/src/pages/crowdnewsroom/[id]/map.vue` accordingly. - @maptiler/geocoding-control 3 swapped its DOM-event API for an `Evented`-style `.on(type, handler)` and exposes the picked feature as `event.feature` instead of `event.detail`. CSS is shipped via Lit shadow DOM, so the legacy `style.css` import is dropped. - `vue-maplibre-gl@5` references `geojson` at runtime even though it declares it as a devDependency, so add it as an explicit dependency.
maplibre-gl 5 declares `"type": "module"` and ships a native ESM build. Vite's dep-optimizer (rolldown-based since v8) still wraps it in a CJS-to-ESM shim, where the re-exported symbol gets renamed for tree-shaking. The optimized `maplibre-gl.js` and the consumer chunks (`vue-maplibre-gl.js`, `@maptiler_geocoding-control_*.js`) are bundled in separate passes and can disagree on the rename — the result is a runtime `SyntaxError: doesn't provide an export named: 't'` the moment a callout-map page loads. Adding `optimizeDeps.exclude: ['maplibre-gl']` lets the browser load the package directly. The two consumers continue resolving `from 'maplibre-gl'` against the same native ESM module, so the import names line up.
@maptiler/geocoding-control v3 split the legacy DOM `pick` event into
two distinct flows:
* `pick` - fires repeatedly as the user navigates the suggestion
list (keyboard arrows, hover); `event.feature` reflects
the currently *previewed* candidate and may be null when
the user moves away.
* `select` - fires once, when the user actually picks a result
(mouse click / Enter); `event.feature` is the chosen one.
The map's red selection ring (and the geocode marker that drives it)
should follow the confirmed selection. Listening on `pick` made the
ring blink in/out as the user hovered the suggestions and disappear
the moment they actually clicked, which is what surfaced as "selecting
an address opens the side panel but the marker is gone".
…source maplibre-gl 5 tightened its worker-IPC serializer. Features returned by `Map.queryRenderedFeatures` carry an internal prototype that is not in the worker's class registry, so handing them back into a `MglGeoJsonSource` (the SELECTED_RESPONSE source on click) explodes inside the source's `_updateWorkerData` with: Error: can't serialize object of unregistered class Xf Round-trip through JSON.stringify/parse to drop the prototype and hand the source a plain GeoJSON Feature. This is the same fix maplibre's own examples now apply for the queryRenderedFeatures → GeoJSONSource flow.
`optimizeDeps.exclude: ['maplibre-gl']` was the wrong fix: the package ships UMD inside a `type: module` outer shell (its `dist/package.json` re-declares `type: commonjs`), so loading it as native ESM in the browser fails with `doesn't provide an export named: 'default'`. Switch to `optimizeDeps.include` for the three packages that share maplibre-gl (`maplibre-gl`, `vue-maplibre-gl`, `@maptiler/geocoding-control/maplibregl`). Listing them explicitly forces Vite's dep-optimizer to bundle them in a single pass, so the rolldown-renamed re-export symbol stays consistent across all three chunks and the original `doesn't provide an export named: 't'` runtime error disappears.
The previous JSON.stringify/parse round-trip stripped maplibre's internal prototype, but it also dropped `geometry` — Mapbox/MapLibre expose `feature.geometry` via a non-enumerable getter that JSON.stringify silently skips. Without geometry the SELECTED_RESPONSE GeoJSON source still mounted (no error, sidebar opened correctly via properties), but the red-ring circle layer had nothing to draw, so the selected point looked unmarked. Build the plain feature explicitly via destructuring; this triggers the getter for `geometry` and `properties`, gives us a plain GeoJSON Feature, and avoids the worker-IPC class-registry error.
`findSelectedFeature` resolves the selected response via
`queryRenderedFeatures`, which returns whichever feature is on top —
often the *cluster* the response belongs to, not the response point
itself. The fixed `circle-radius: 20` red ring then sat exactly on
the cluster's filled black circle (which uses the same 20-40 px step
schedule), so the 2 px stroke was hidden inside the cluster's fill
and the user saw no marker at all.
Make the ring data-driven: when the selected feature is a cluster
(`['has', 'point_count']`), use `cluster_radius + 5` so the ring sits
just outside the black circle; otherwise stay at a fixed 22 px around
the unclustered icon. Stroke widened to 3 for parity at higher zooms.
Also add `console.debug('[map] ...')` lines around the selection flow
so it's easier to spot whether `geometry`/`properties` survive the
class-strip and which layer the matched feature came from.
41d4311 to
64416bf
Compare
# Conflicts: # yarn.lock
The SELECTED_RESPONSE GeoJSON source was wrapped in v-if="selectedFeature" and fed a single GeoJSON Feature. With vue-maplibre-gl 5 this combination intermittently produces no rendered ring even though selectedFeature is correctly populated and the side panel opens — the source/layer pair gets torn down and re-mounted on every selection change, and the layer ends up below the icon stack (or is missed entirely) by the time the next paint runs. Switch to the canonical form: a permanently mounted source bound to a computed FeatureCollection that is empty while nothing is selected. The circle layer is added once during map mount, lives at the top of the stack for the lifetime of the page, and just renders zero / one features depending on selection state. Also widens 'circle-color' from the legacy 'transparent' keyword to an explicit 'rgba(0, 0, 0, 0)' to dodge any maplibre-5 colour-parser strictness.
# Conflicts: # yarn.lock
The MglCircleLayer approach kept misbehaving on maplibre-gl 5: the SELECTED_RESPONSE GeoJSONSource accepted and stored the feature (getSource().getData() returns it), but the GPU pass never rendered anything. queryRenderedFeatures on the layer returned an empty array even with forced paint of \`circle-radius: 50\`, \`circle-opacity: 1\`, \`circle-color: red\` — i.e. nothing landed on the canvas at all. Looks like a v5 worker-IPC quirk with non-clustered GeoJSON sources that get their data set dynamically through setData. Bypass the style stack: create a raw maplibre Marker with a custom HTML element (red border, transparent fill) at the selected feature's coordinates. A single watch on [selectedFeature, mapLoaded] adds / updates / removes the marker. No GeoJSON source, no CircleLayer, no layer-order / paint-spec worries. Also fix a stray error from the cluster-source-loaded watcher firing before the UNCLUSTERED_POINTS layer has been added: filter the \`layers\` arg of queryRenderedFeatures to layers that actually exist. maplibre 5 throws on unknown layer ids, where v3 silently returned [].
Collaborator
Author
# Conflicts: # yarn.lock
# Conflicts: # yarn.lock
wpf500
reviewed
May 27, 2026
Member
wpf500
left a comment
There was a problem hiding this comment.
I'm not convinced by the replacement of MglCircleLayer with all this custom code. There must be an underlying reason why it has stopped working?
Unless there is a really good reason to upgrade this as a priority I would leave this change until there is time to look into the underlying reason.
Member
|
Closing for now until this upgrade becomes urgent or we find a better solution for |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Tracking: https://correctivdigital.openproject.com/projects/beabee/work_packages/2196
Summary
Bumps the map stack in
apps/frontend:maplibre-glvue-maplibre-gl@maptiler/client@maptiler/geocoding-controlgeojsonCode adjustments
Map.loadImageis Promise-returning in maplibre-gl 5 —apps/frontend/src/utils/images.tsrewritten toawait mapInstance.loadImage(...).then(({ data }) => data).GeoJSONSource.getClusterExpansionZoomis also Promise-returning —apps/frontend/src/pages/crowdnewsroom/[id]/map.vueupdated.@maptiler/geocoding-control@3dropped the DOM-styleaddEventListenerand the standalonestyle.cssexport. The control now extendsEvented, so thepicklistener uses.on('pick', handler)and reads the newevent.feature. The CSS is delivered through the Lit shadow DOM, so the previous CSS import is removed.vue-maplibre-gl@5internally importsgeojsonat runtime even though it declares it only as a devDependency — added as an explicit dep inapps/frontend.MglCircleLayeragainst a dedicatedSELECTED_RESPONSEGeoJSON source kept silently rendering nothing on maplibre-gl 5: the source held the feature, but no paint reached the canvas (queryRenderedFeatures returned[]even with forcedradius: 50,opacity: 1,color: red). Replaced with a raw maplibreMarkerwith a custom HTML element (red border, transparent fill), driven by a single watch on[selectedFeature, mapLoaded]. Sidesteps the style/source pipeline entirely.queryRenderedFeatureslayer filter — maplibre-gl 5 throws on unknown layer ids; v3 silently returned[]. The cluster-sourcesourcedatawatcher can fire before theunclustered-pointslayer has mounted (it's guarded byv-if="mapLoaded"to wait for icons), sofindSelectedFeaturenow filters itslayersarg to the ones that actually exist.Verification
yarn check✅yarn build✅Notes
The selection-ring stayed broken under several earlier attempts (always-mounted source with empty FeatureCollection,
markRaw'd feature to dodge Vue reactivity,circle-opacity: 0instead of alpha-0 color,v-if="mapLoaded"ordering, explicitmoveLayerafter each selection). None of them produced a rendered ring, which is why this lands as a raw Marker rather than a fix to the source/layer setup.