Skip to content

LeafletMap | MapLibre: Ship a bundler-independent worker - #922

Open
rokotyan wants to merge 4 commits into
f5:mainfrom
rokotyan:fix/maplibre-worker
Open

rokotyan wants to merge 4 commits into
f5:mainfrom
rokotyan:fix/maplibre-worker

Conversation

@rokotyan

Copy link
Copy Markdown
Contributor

Fixes #921

With maplibre-gl 6, LeafletMap's MapLibre renderer silently renders no tiles in apps without maplibre-specific bundler config. This makes the published @unovis/ts work with zero consumer configuration, and removes the ?maplibreWorkerAsset marker and the dev-gallery webpack rules that supported it.

  • Core
  • Dev examples (webpack config only, no new examples)
  • Docs (CSP guide)
  • Wrappers (not affected)
  • Gallery example (not affected)

What changed

  • packages/ts/vite-plugin-maplibre-worker-source.ts serves a virtual module: the installed maplibre-gl/dist/maplibre-gl-worker.mjs bundled into one self-contained IIFE (via Vite's build() API, the worker file as entry) and exported as a string, plus the maplibre version it was built from. It's emitted as dist/components/leaflet-map/modules/maplibre-worker-source.js, so consumers get an ordinary ES module.
  • map.ts imports it lazily in the MapLibre branch and calls setWorkerUrl(URL.createObjectURL(new Blob([source], { type: 'text/javascript' }))). The if (!maplibre.getWorkerUrl()) guard is kept, so a consumer's own setWorkerUrl() still wins. The Blob URL is created once and never revoked: maplibre re-reads the URL for every worker in its pool and again when the pool is rebuilt.
  • The plugin fails the build if the worker's top-level self.worker = new Worker(self) registration is missing, if the chunk still imports anything, or if it uses import.meta. maplibre marks its dist as side-effect free, so a tree-shaken empty worker otherwise builds "successfully".
  • LeafletMap warns when maplibre.getVersion() differs from the version the worker was built from, so a main-thread/worker mismatch is no longer silent.
  • The dev gallery compiles packages/ts/src with webpack, which can't resolve a Vite virtual module and treats virtual: as a URL scheme (so resolve.alias doesn't apply). A NormalModuleReplacementPlugin maps the id to the built file in packages/ts/dist. The maplibre-gl.mjs parser rule stays: without it webpack's "Critical dependency" warning returns.

Why this design

Alternatives and why they were rejected are in #921. In short: Vite's ?worker&url has the same import.meta.url failure modes; ?worker&inline exports only a Worker factory and revokes its Blob URL on start, while maplibre's only hook is setWorkerUrl(string); a postinstall build is skipped by pnpm 10 and --ignore-scripts; a CDN worker URL adds a third-party runtime dependency.

CSP

Consumers rendering the MapLibre renderer need worker-src 'self' blob:. maplibre itself already uses Blob workers for cross-origin URLs. The CSP guide now says so. (Side note: the guide previously claimed the ELK graph spawns a Blob worker; elk.bundled.js runs its layout in-thread via a fake worker, so that directive was never needed there. Happy to correct that in this PR or separately.)

Verification

Throwaway Vite app consuming the packed dist via a file: tarball, with no maplibre-specific config:

Scenario Result
vite build, served by a static server mimicking deployed nginx (default_type application/octet-stream, stock mime.types, SPA fallback) Tiles render. Worker URL is blob:. Only .js assets requested; no .mjs, no maplibre-gl-shared*, no failed requests, no console errors.
vite dev with default dependency pre-bundling Tiles render. Worker module served from node_modules/.vite/deps/.
maplibregl.setWorkerUrl('/custom-worker.js') before the map mounts Respected: that file is fetched, the bundled worker module is never loaded, tiles render.
Dev gallery (pnpm dev, webpack) Vector Map example Tiles render, compiles with zero warnings.

Also: pnpm build (core), pnpm build:dev, lint and type-check clean for the touched files. A second pnpm build is skipped by the .srcsha cache, whose key now includes package.json.

Consumer cleanup this unblocks

Apps that worked around this can delete: their ?worker&url bundle of maplibre-gl-worker.mjs and the setWorkerUrl() call that used it, a direct maplibre-gl dependency added only for that, and any Vite external/entryFileNames tweaks that kept the worker unhashed. Bundler asset rules emitting maplibre-gl-{worker,shared}.mjs can go too.

Open question for maintainers

maplibre-gl is declared as ^6.7.0 here. Since the worker is frozen at publish time while the main-thread maplibre-gl resolves at the consumer's install time, an exact pin would make the two match by construction (package managers nest a matching copy for us). The runtime warning covers the mismatch either way; pinning exactly is a one-line change if you prefer it.

🤖 Generated with Claude Code

rokotyan and others added 3 commits September 15, 2026 13:55
…b URL

maplibre-gl 6 locates its worker via `import.meta.url`, which bundlers can't reliably
follow for a dependency: webpack ignores it, and Vite copies the `.mjs` worker verbatim
so stock servers reject it as a module worker and its content-hashed `maplibre-gl-shared`
sibling can't be found. Tiles then silently fail to render.

A Vite plugin now serves `virtual:maplibre-worker-source`: the installed maplibre-gl
worker bundled into a self-contained script and exported as a string, emitted next to
`map.js` in `dist`. `LeafletMap` starts the worker from a Blob URL of it unless the
consumer set a worker URL themselves, so no bundler configuration is needed. The plugin
asserts the worker's top-level registration survived bundling so an empty tree-shaken
worker can't ship, and the build cache key now includes `package.json`.

`maplibre-gl` is now a `^6.7.0` range. Since the worker is frozen at build time while
the main-thread maplibre-gl resolves at install time, `LeafletMap` warns when the two
versions differ, so a protocol mismatch is no longer a silent failure.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Drops the `?maplibreWorkerAsset` asset rule and maps `virtual:maplibre-worker-source`
to the built copy in `packages/ts/dist`, since webpack treats `virtual:` as a URL
scheme and can't alias it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@rokotyan

rokotyan commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

@lee00678 I've not tested it properly yet on my end. The MapLibre update was kind of a breaking change so it would be nice to fix and publish as 1.7.1

@lee00678

Copy link
Copy Markdown
Collaborator

I was just about to ask if you've tested this on your end. Because we don't have a good source of verification other than what we use here and the dev examples we have.

@lee00678

Copy link
Copy Markdown
Collaborator

@rokotyan I think this looks good, we plan on release 1.7.1 tmw. Flag me if you want us to hold off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LeafletMap | MapLibre: worker fails silently without consumer bundler config

3 participants