You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/09/10 02:51:19 UTC

(superset) branch master updated: fix(maplibre): wire up maplibre-gl 6's worker for plugin-chart-point-cluster-map (#44042)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 1820a162dbe fix(maplibre): wire up maplibre-gl 6's worker for plugin-chart-point-cluster-map (#44042)
1820a162dbe is described below

commit 1820a162dbe3964d3ad11cf0ff6cabe4aa8df6d9
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Wed Sep 9 19:51:11 2026 -0700

    fix(maplibre): wire up maplibre-gl 6's worker for plugin-chart-point-cluster-map (#44042)
    
    Co-authored-by: Claude Code <no...@anthropic.com>
---
 .../src/MapLibre.tsx                               | 15 ++++++++--
 .../src/mapLibreDefaults.ts                        | 24 ++++++++++++++++
 .../src/transformProps.ts                          |  2 +-
 .../test/MapLibre.test.tsx                         | 33 ++++++++++++++++++++++
 .../types/global.d.ts                              | 23 +++++++++++++++
 superset-frontend/spec/helpers/setup.ts            |  7 +++++
 superset-frontend/webpack.config.js                | 18 +++++++++++-
 7 files changed, 117 insertions(+), 5 deletions(-)

diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/src/MapLibre.tsx b/superset-frontend/plugins/plugin-chart-point-cluster-map/src/MapLibre.tsx
index d1f875804a3..a9b4603a754 100644
--- a/superset-frontend/plugins/plugin-chart-point-cluster-map/src/MapLibre.tsx
+++ b/superset-frontend/plugins/plugin-chart-point-cluster-map/src/MapLibre.tsx
@@ -19,6 +19,7 @@
 import { memo, useCallback, useEffect, useState } from 'react';
 import { Map as MapLibreMap } from 'react-map-gl/maplibre';
 import { Map as MapboxMap } from 'react-map-gl/mapbox';
+import * as maplibregl from 'maplibre-gl';
 import { WebMercatorViewport } from '@math.gl/web-mercator';
 import {
   resolveMapStyle,
@@ -28,13 +29,21 @@ import { useTheme } from '@apache-superset/core/theme';
 import { t } from '@apache-superset/core/translation';
 import ScatterPlotOverlay from './components/ScatterPlotOverlay';
 import { getMapboxApiKey } from './utils/mapbox';
+import { DEFAULT_POINT_RADIUS } from './mapLibreDefaults';
 import 'maplibre-gl/dist/maplibre-gl.css';
 import './MapLibre.css';
 
-const DEFAULT_MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
+// maplibre-gl 6's ESM-only build derives its worker URL from
+// `import.meta.url`, which our (non-ESM-output) webpack bundle rewrites to
+// a build-time path that doesn't match maplibre's `^https?:` check, so
+// `WORKER_URL` silently resolves to `''` and no worker gets created. Point
+// it at the copy CopyPlugin emits alongside the rest of our static assets
+// (see webpack.config.js) instead, using the runtime-configurable public
+// path so this also works behind a reverse-proxy / APPLICATION_ROOT
+// prefix. This must run before the first MapLibre map mounts.
+maplibregl.setWorkerUrl(`${__webpack_public_path__}maplibre-gl-worker.mjs`);
 
-export const DEFAULT_MAX_ZOOM = 16;
-export const DEFAULT_POINT_RADIUS = 60;
+const DEFAULT_MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
 
 interface Viewport {
   longitude: number;
diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/src/mapLibreDefaults.ts b/superset-frontend/plugins/plugin-chart-point-cluster-map/src/mapLibreDefaults.ts
new file mode 100644
index 00000000000..70adceb1225
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-point-cluster-map/src/mapLibreDefaults.ts
@@ -0,0 +1,24 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Kept in their own module (rather than exported from MapLibre.tsx) so that
+// transformProps.ts can use them without pulling in MapLibre.tsx's
+// `maplibre-gl` import and its module-level setWorkerUrl() side effect.
+export const DEFAULT_MAX_ZOOM = 16;
+export const DEFAULT_POINT_RADIUS = 60;
diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-point-cluster-map/src/transformProps.ts
index b70495ec7e3..04f67341b13 100644
--- a/superset-frontend/plugins/plugin-chart-point-cluster-map/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-point-cluster-map/src/transformProps.ts
@@ -21,7 +21,7 @@ import Supercluster, {
 } from 'supercluster';
 import { ChartProps, getMapProviderMapStyle } from '@superset-ui/core';
 import { t } from '@apache-superset/core/translation';
-import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapLibre';
+import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './mapLibreDefaults';
 import roundDecimal from './utils/roundDecimal';
 
 const NOOP = () => {};
diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/test/MapLibre.test.tsx b/superset-frontend/plugins/plugin-chart-point-cluster-map/test/MapLibre.test.tsx
index 1db0cd24bdc..7bf5a353d2c 100644
--- a/superset-frontend/plugins/plugin-chart-point-cluster-map/test/MapLibre.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-point-cluster-map/test/MapLibre.test.tsx
@@ -70,6 +70,23 @@ jest.mock('@apache-superset/core/theme', () => ({
 jest.mock('maplibre-gl/dist/maplibre-gl.css', () => ({}));
 jest.mock('../src/MapLibre.css', () => ({}));
 
+// maplibre-gl 6 is an ESM-only package with no "require"/"default" export
+// condition, so jest's (CJS-based) resolver can't locate the real module to
+// mock over it by name; { virtual: true } skips that resolution step.
+//
+// The mock jest.fn() is created inline (not hoisted out to a `const`)
+// because MapLibre.tsx calls maplibregl.setWorkerUrl() synchronously at
+// import time, and `import`/jest.mock() calls are themselves hoisted above
+// this file's plain `const` declarations — a `const` referenced here would
+// still be in its temporal dead zone when that import-time call fires.
+jest.mock(
+  'maplibre-gl',
+  () => ({ __esModule: true, setWorkerUrl: jest.fn() }),
+  { virtual: true },
+);
+
+// eslint-disable-next-line import/first
+import * as maplibregl from 'maplibre-gl';
 // eslint-disable-next-line import/first
 import MapLibre from '../src/MapLibre';
 
@@ -94,6 +111,22 @@ const defaultProps = {
   onViewportChange: jest.fn(),
 };
 
+// Captured before the first jest.clearAllMocks() below wipes the call the
+// module made at import time.
+const setWorkerUrlCallAtImport = [
+  ...jest.mocked(maplibregl.setWorkerUrl).mock.calls,
+];
+
+test('points maplibre-gl at the CopyPlugin-emitted worker asset before any map can mount', () => {
+  // maplibre-gl 6's ESM-only build derives its worker URL from
+  // `import.meta.url`, which webpack rewrites to a build-time path that
+  // doesn't match maplibre's `^https?:` check, so the worker silently
+  // fails to start unless setWorkerUrl() is called first (see MapLibre.tsx).
+  expect(setWorkerUrlCallAtImport).toEqual([
+    ['/static/assets/maplibre-gl-worker.mjs'],
+  ]);
+});
+
 beforeEach(() => {
   lastMapProps = {};
   document.body.innerHTML = '';
diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/types/global.d.ts b/superset-frontend/plugins/plugin-chart-point-cluster-map/types/global.d.ts
new file mode 100644
index 00000000000..bffb6dd14a2
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-point-cluster-map/types/global.d.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// webpack's runtime-configurable public path, mirroring src/globals.d.ts.
+// Used to point maplibre-gl at its CopyPlugin-emitted worker asset (see
+// MapLibre.tsx) with the correct app-root / reverse-proxy prefix applied.
+declare var __webpack_public_path__: string;
diff --git a/superset-frontend/spec/helpers/setup.ts b/superset-frontend/spec/helpers/setup.ts
index 3d216e00af1..9888cad0f94 100644
--- a/superset-frontend/spec/helpers/setup.ts
+++ b/superset-frontend/spec/helpers/setup.ts
@@ -37,6 +37,13 @@ expect.extend(matchers);
 // Allow JSX tests to have React import readily available
 global.React = React;
 
+// webpack's runtime-configurable public path (see src/public-path.ts and
+// src/globals.d.ts). Modules that reference it at import time (e.g. to
+// build a static-asset URL, such as plugin-chart-point-cluster-map's
+// MapLibre.tsx) would otherwise throw ReferenceError under jest, since
+// nothing else in the test environment ever sets this webpack-only global.
+global.__webpack_public_path__ = '/static/assets/';
+
 // Mock ace-builds globally for tests
 jest.mock('ace-builds/src-min-noconflict/mode-handlebars', () => ({}));
 jest.mock('ace-builds/src-min-noconflict/mode-css', () => ({}));
diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js
index 84bb13d7b5d..3ad02476f3a 100644
--- a/superset-frontend/webpack.config.js
+++ b/superset-frontend/webpack.config.js
@@ -151,7 +151,23 @@ const plugins = [
   }),
 
   new CopyPlugin({
-    patterns: ['package.json', { from: 'src/assets/images', to: 'images' }],
+    patterns: [
+      'package.json',
+      { from: 'src/assets/images', to: 'images' },
+      // maplibre-gl 6's ESM-only build loads its worker from a real URL
+      // instead of an inlined blob (see MapLibre.tsx for the matching
+      // maplibregl.setWorkerUrl() call). The worker's own bundle does a
+      // relative ESM import of its "shared" chunk, so both files must be
+      // copied verbatim, unhashed, into the same output directory.
+      {
+        from: 'node_modules/maplibre-gl/dist/maplibre-gl-worker.mjs',
+        to: 'maplibre-gl-worker.mjs',
+      },
+      {
+        from: 'node_modules/maplibre-gl/dist/maplibre-gl-shared.mjs',
+        to: 'maplibre-gl-shared.mjs',
+      },
+    ],
   }),
 
   // static pages