From 821a8fb471e93ac770e5d19902ae015e150c1766 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Wed, 26 Aug 2026 15:52:35 +0200 Subject: [PATCH] Expose a worker override from the ESM entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default bundle exposes `mapboxgl.workerUrl` and `mapboxgl.workerClass` as documented accessors. The ESM entry has the counterpart to the first one only, `setWorkerUrl`; there is no way to hand GL JS a worker the application built itself. That gap is what makes the ESM entry unusable under some bundlers. `dist/esm/core.js` spawns its worker with `new Worker(new URL('worker.js', import.meta.url))`, which relies on the host bundler recognising the pattern and emitting the worker as its own chunk. Vite and webpack 5 do that for dependencies. Angular's esbuild-based `@angular/build:application` builder implements it as a TypeScript transformer, so it only ever runs over the application's own sources and never over prebuilt JS in node_modules. The expression then survives verbatim into the output, no worker file is emitted, and the request resolves against the emitted chunk's URL. Behind an SPA fallback that answers with index.html and status 200 the worker dies silently: the main thread keeps running and only tile parsing stops. `setWorkerUrl` can paper over this if the application copies `dist/esm` out as static assets, but such copies carry no content hash and go stale on upgrade. Two shapes are implemented here; happy to drop either one. `setWorkerClass(klass)` mirrors the existing `workerClass` field that `web_worker.ts` already honours, so it adds no new concept. `setWorkerFactory(create)` does the same in a shape that type-checks. An application that has to construct the worker itself can only return an instance, and TypeScript cannot express a constructor returning an unrelated object, so every `workerClass` caller ends up asserting the type — including the workaround in #13700: mapboxgl.workerClass = class { constructor() { return new Worker(new URL(...), {type: 'module'}); } } as unknown as new () => Worker; Precedence in the ESM `createWorker` is factory, then class, then url, then the existing `import.meta.url` default. If the factory would be welcome on the default entry as well, that is a `mapboxgl.workerFactory` accessor in src/index.ts plus the same check in web_worker.ts, and it would remove the assertion from the #13700 workaround too. Refs #13700, #13678 --- src/index.esm.ts | 2 +- src/util/web_worker_esm_npm.ts | 5 +++++ src/util/worker_class.ts | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/index.esm.ts b/src/index.esm.ts index 6fdb7b27933..c83f9e54fe3 100644 --- a/src/index.esm.ts +++ b/src/index.esm.ts @@ -173,7 +173,7 @@ export {setRTLTextPlugin, getRTLTextPluginStatus} from './source/rtl_text_plugin export {setSdkInfo} from './util/mapbox'; export {addTileProvider} from './source/tile_provider'; export {prewarm, clearPrewarmedResources} from './util/worker_pool_factory'; -export {setWorkerUrl} from './util/worker_class'; +export {setWorkerUrl, setWorkerClass, setWorkerFactory} from './util/worker_class'; export {getWorkerCount, setWorkerCount} from './util/worker_pool'; export {setAccessToken, setBaseApiUrl, setMaxParallelImageRequests, getDracoUrl, setDracoUrl, getMeshoptUrl, setMeshoptUrl, getBuildingGenUrl, setBuildingGenUrl} from './util/config'; diff --git a/src/util/web_worker_esm_npm.ts b/src/util/web_worker_esm_npm.ts index c922fcb1b7c..01316ce51e4 100644 --- a/src/util/web_worker_esm_npm.ts +++ b/src/util/web_worker_esm_npm.ts @@ -5,6 +5,11 @@ import WorkerClass from './worker_class'; // its dependencies into a separate chunk. Also works when self-hosted // same-origin without a bundler. export function createWorker(): Worker { + if (WorkerClass.workerFactory) return WorkerClass.workerFactory(); + + // eslint-disable-next-line new-cap + if (WorkerClass.workerClass != null) return new WorkerClass.workerClass(); + if (WorkerClass.workerUrl) { return new Worker(WorkerClass.workerUrl, {type: 'module'}); } diff --git a/src/util/worker_class.ts b/src/util/worker_class.ts index 046df32d74a..b941c4776a7 100644 --- a/src/util/worker_class.ts +++ b/src/util/worker_class.ts @@ -3,12 +3,14 @@ import type {Class} from '../types/class'; type WorkerState = { workerUrl: string; workerClass: Class | null; + workerFactory: (() => Worker) | null; workerParams?: WorkerOptions; // Internal, test-only: extra options passed to `new Worker()` }; const WorkerClass: WorkerState = { workerUrl: '', workerClass: null, + workerFactory: null, workerParams: undefined, }; @@ -24,4 +26,37 @@ export function setWorkerUrl(url: string) { WorkerClass.workerUrl = url; } +/** + * Sets a constructor used to create the WebWorker. Must be set once, before the + * first `new Map(...)`. Counterpart to `mapboxgl.workerClass` on the default + * bundle, and takes precedence over {@link setWorkerUrl}. + * + * Bundlers that only rewrite the `new Worker(new URL(...), ...)` pattern in the + * application's own sources — Angular's esbuild-based builder, for example — + * never see the equivalent expression inside this package and therefore emit no + * worker chunk for it. Passing a constructor lets the application spell that + * pattern out itself and hand the resulting worker back. + * + * @param {Class} klass A constructor returning a WebWorker running the GL JS worker bundle. + */ +export function setWorkerClass(klass: Class | null) { + WorkerClass.workerClass = klass; +} + +/** + * Sets a function used to create the WebWorker. Must be set once, before the + * first `new Map(...)`. Takes precedence over {@link setWorkerClass} and + * {@link setWorkerUrl}. + * + * Serves the same purpose as {@link setWorkerClass}, in a shape that type-checks: + * an application that has to construct the worker itself can only return an + * instance, and a constructor returning an unrelated object cannot be expressed + * in TypeScript, so every `workerClass` caller ends up asserting the type. + * + * @param {Function} create A function returning a WebWorker running the GL JS worker bundle. + */ +export function setWorkerFactory(create: (() => Worker) | null) { + WorkerClass.workerFactory = create; +} + export default WorkerClass;