Skip to content

Commit 2e7d4e5

Browse files
committed
fix(map): support MapLibre v6 runtime
1 parent 83544e7 commit 2e7d4e5

21 files changed

Lines changed: 371 additions & 31 deletions

apps/web/scripts/build-runtime-modules.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ import { mkdirSync, writeFileSync } from "node:fs";
2929
import { dirname, resolve } from "node:path";
3030
import { fileURLToPath } from "node:url";
3131
import { build } from "esbuild";
32+
import { copyMapLibreRuntimeAssets } from "./maplibre-runtime.mjs";
3233

3334
const __dirname = dirname(fileURLToPath(import.meta.url));
3435
const webRoot = resolve(__dirname, "..");
3536
const outDir = resolve(webRoot, "public/runtime");
3637

3738
mkdirSync(outDir, { recursive: true });
39+
copyMapLibreRuntimeAssets(outDir);
3840

3941
const GLOBAL = "globalThis.__OMX_RUNTIME__";
4042

apps/web/scripts/build-sw.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
// 2. the local git short SHA (dev builds)
1010
// 3. a timestamp fallback (no env, no git)
1111
import { execFileSync } from "node:child_process";
12+
import { dirname, resolve } from "node:path";
13+
import { fileURLToPath } from "node:url";
1214
import { build } from "esbuild";
15+
import { copyMapLibreRuntimeAssets, maplibreVersion } from "./maplibre-runtime.mjs";
16+
17+
const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
1318

1419
export function resolveSwBuildId(env = process.env) {
1520
const fromEnv = (env.SW_BUILD_ID ?? "").trim();
@@ -28,6 +33,7 @@ export function resolveSwBuildId(env = process.env) {
2833

2934
if (process.argv[1]?.endsWith("build-sw.mjs")) {
3035
const buildId = resolveSwBuildId();
36+
copyMapLibreRuntimeAssets(resolve(webRoot, "public/runtime"));
3137
await build({
3238
entryPoints: ["src/sw.ts"],
3339
bundle: true,
@@ -36,6 +42,7 @@ if (process.argv[1]?.endsWith("build-sw.mjs")) {
3642
platform: "browser",
3743
define: {
3844
"process.env.NODE_ENV": '"production"',
45+
__MAPLIBRE_VERSION__: JSON.stringify(maplibreVersion),
3946
__SW_BUILD_ID__: JSON.stringify(buildId),
4047
},
4148
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { copyFileSync, mkdirSync, readFileSync } from "node:fs";
2+
import { createRequire } from "node:module";
3+
import { dirname, resolve } from "node:path";
4+
5+
const require = createRequire(import.meta.url);
6+
const packageJsonPath = require.resolve("maplibre-gl/package.json");
7+
const packageRoot = dirname(packageJsonPath);
8+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
9+
10+
if (
11+
typeof packageJson.version !== "string" ||
12+
!/^[A-Za-z0-9._+-]{1,128}$/.test(packageJson.version)
13+
) {
14+
throw new Error("maplibre-gl package has an invalid version");
15+
}
16+
17+
export const maplibreVersion = packageJson.version;
18+
19+
const RUNTIME_FILES = ["maplibre-gl-worker.mjs", "maplibre-gl-shared.mjs"];
20+
21+
export function copyMapLibreRuntimeAssets(runtimeRoot) {
22+
const versionDir = resolve(runtimeRoot, "maplibre-gl", maplibreVersion);
23+
mkdirSync(versionDir, { recursive: true });
24+
for (const filename of RUNTIME_FILES) {
25+
copyFileSync(resolve(packageRoot, "dist", filename), resolve(versionDir, filename));
26+
}
27+
}

apps/web/src/app/settings/offline/AreaPickerMap.test.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ vi.mock("@/components/map/MapCredits", () => ({
1919
}));
2020

2121
vi.mock("maplibre-gl", () => {
22+
let workerUrl = "";
2223
class FakeMap {
2324
static instances: FakeMap[] = [];
25+
static workerUrlsAtConstruction: string[] = [];
2426

2527
remove = vi.fn();
2628
touchZoomRotate = { disableRotation: vi.fn() };
@@ -29,6 +31,7 @@ vi.mock("maplibre-gl", () => {
2931

3032
constructor() {
3133
FakeMap.instances.push(this);
34+
FakeMap.workerUrlsAtConstruction.push(workerUrl);
3235
}
3336

3437
on = vi.fn((event: unknown, handler: unknown) => {
@@ -56,7 +59,14 @@ vi.mock("maplibre-gl", () => {
5659
}
5760
}
5861

59-
return { Map: FakeMap };
62+
return {
63+
getVersion: () => "6.1.0",
64+
getWorkerUrl: () => workerUrl,
65+
Map: FakeMap,
66+
setWorkerUrl: (url: string) => {
67+
workerUrl = url;
68+
},
69+
};
6070
});
6171

6272
import * as maplibregl from "maplibre-gl";
@@ -67,10 +77,12 @@ const fakeMapClass = maplibregl.Map as unknown as {
6777
remove: ReturnType<typeof vi.fn>;
6878
trigger: (event: string) => void;
6979
}>;
80+
workerUrlsAtConstruction: string[];
7081
};
7182

7283
afterEach(() => {
7384
fakeMapClass.instances.length = 0;
85+
fakeMapClass.workerUrlsAtConstruction.length = 0;
7486
});
7587

7688
describe("AreaPickerMap", () => {
@@ -82,6 +94,9 @@ describe("AreaPickerMap", () => {
8294
);
8395

8496
await waitFor(() => expect(fakeMapClass.instances).toHaveLength(1));
97+
expect(fakeMapClass.workerUrlsAtConstruction).toEqual([
98+
"/runtime/maplibre-gl/6.1.0/maplibre-gl-worker.mjs",
99+
]);
85100

86101
rerender(
87102
<AreaPickerMap initialCenter={[10.45, 51.16]} initialZoom={4} onChange={latestOnChange} />,

apps/web/src/app/settings/offline/AreaPickerMap.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createGeoJsonSourceDataBridge } from "@/components/map/layers/layerStyl
99
import { MapCredits } from "@/components/map/MapCredits";
1010
import { useEnv } from "@/lib/EnvProvider";
1111
import { baseMapCreditsHtml, loadMaptilerStyle, loadOpenMapXStyle } from "@/lib/map";
12+
import { loadMapLibreRuntime } from "@/lib/maplibreRuntime";
1213
import type { OfflinePackageBbox } from "@/lib/offlineAreas";
1314

1415
const BOUNDARY_SOURCE = "picker-boundary-source";
@@ -97,7 +98,7 @@ export function AreaPickerMap({ initialCenter, initialZoom, onChange, fitBbox, b
9798
let replayBoundary: (() => void) | null = null;
9899

99100
const init = async () => {
100-
const maplibregl = await import("maplibre-gl");
101+
const maplibregl = await loadMapLibreRuntime();
101102
if (destroyed || !containerRef.current) return;
102103
const style =
103104
env.styleProvider === "openmapx"

apps/web/src/app/settings/offline/OfflineMapView.test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,30 @@ vi.mock("@/lib/offlineAreas", () => {
3434
});
3535

3636
vi.mock("maplibre-gl", () => {
37+
let workerUrl = "";
3738
class FakeMap {
39+
static workerUrlsAtConstruction: string[] = [];
40+
3841
touchZoomRotate = { disableRotation: vi.fn() };
3942
keyboard = { disableRotation: vi.fn() };
4043
on = vi.fn();
4144
remove = vi.fn();
45+
46+
constructor() {
47+
FakeMap.workerUrlsAtConstruction.push(workerUrl);
48+
}
4249
}
43-
return { Map: FakeMap };
50+
return {
51+
getVersion: () => "6.1.0",
52+
getWorkerUrl: () => workerUrl,
53+
Map: FakeMap,
54+
setWorkerUrl: (url: string) => {
55+
workerUrl = url;
56+
},
57+
};
4458
});
4559

60+
import * as maplibregl from "maplibre-gl";
4661
import * as offlineAreas from "@/lib/offlineAreas";
4762
import { OfflineMapView } from "./OfflineMapView";
4863

@@ -54,6 +69,7 @@ const mocks = (
5469
};
5570
}
5671
).__test;
72+
const fakeMapClass = maplibregl.Map as unknown as { workerUrlsAtConstruction: string[] };
5773

5874
const record = {
5975
id: `omp2-${"d".repeat(64)}`,
@@ -67,6 +83,7 @@ const record = {
6783
} as OfflinePackageRecord;
6884

6985
afterEach(() => {
86+
fakeMapClass.workerUrlsAtConstruction.length = 0;
7087
vi.clearAllMocks();
7188
});
7289

@@ -76,5 +93,8 @@ describe("OfflineMapView", () => {
7693

7794
await waitFor(() => expect(mocks.refresh).toHaveBeenCalledTimes(1));
7895
expect(mocks.register).toHaveBeenCalledTimes(1);
96+
expect(fakeMapClass.workerUrlsAtConstruction).toEqual([
97+
"/runtime/maplibre-gl/6.1.0/maplibre-gl-worker.mjs",
98+
]);
7999
});
80100
});

apps/web/src/app/settings/offline/OfflineMapView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useEffect, useRef } from "react";
77
import { MapCredits } from "@/components/map/MapCredits";
88
import { useEnv } from "@/lib/EnvProvider";
99
import { baseMapCreditsHtml, loadOpenMapXStyle } from "@/lib/map";
10+
import { loadMapLibreRuntime } from "@/lib/maplibreRuntime";
1011
import {
1112
configureDefaultOfflinePackageResolver,
1213
getDefaultOfflinePackageResolver,
@@ -93,7 +94,7 @@ export function OfflineMapView({ packages, fitTo, height = 360 }: Props) {
9394
const frame = fitTo ?? unionBbox(packages);
9495

9596
void (async () => {
96-
const maplibregl = await import("maplibre-gl");
97+
const maplibregl = await loadMapLibreRuntime();
9798
let resolver = getDefaultOfflinePackageResolver();
9899
if (!resolver) {
99100
resolver = configureDefaultOfflinePackageResolver({
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// @vitest-environment jsdom
2+
3+
import { render, waitFor } from "@testing-library/react";
4+
import { afterEach, describe, expect, it, vi } from "vitest";
5+
6+
vi.mock("@mui/material/styles", async (importOriginal) => ({
7+
...(await importOriginal<typeof import("@mui/material/styles")>()),
8+
useColorScheme: () => ({ mode: "light", systemMode: "light" }),
9+
}));
10+
11+
vi.mock("@/lib/EnvProvider", () => ({
12+
useEnv: () => ({ styleProvider: "openmapx" }),
13+
}));
14+
15+
vi.mock("@/lib/map", () => ({
16+
baseMapCreditsHtml: () => [],
17+
loadMaptilerStyle: vi.fn().mockResolvedValue({}),
18+
loadOpenMapXStyle: vi.fn().mockResolvedValue({}),
19+
}));
20+
21+
vi.mock("./MapCredits", () => ({ MapCredits: () => null }));
22+
23+
vi.mock("maplibre-gl", () => {
24+
let workerUrl = "";
25+
const workerUrlsAtConstruction: string[] = [];
26+
class FakeMap {
27+
flyTo = vi.fn();
28+
remove = vi.fn();
29+
30+
constructor() {
31+
workerUrlsAtConstruction.push(workerUrl);
32+
}
33+
}
34+
class FakeMarker {
35+
addTo = vi.fn(() => this);
36+
setLngLat = vi.fn(() => this);
37+
}
38+
return {
39+
__test: { workerUrlsAtConstruction },
40+
getVersion: () => "6.1.0",
41+
getWorkerUrl: () => workerUrl,
42+
Map: FakeMap,
43+
Marker: FakeMarker,
44+
setWorkerUrl: (url: string) => {
45+
workerUrl = url;
46+
},
47+
};
48+
});
49+
50+
import * as maplibregl from "maplibre-gl";
51+
import { LocationMinimap } from "./LocationMinimap";
52+
53+
const maplibreTest = (maplibregl as unknown as { __test: { workerUrlsAtConstruction: string[] } })
54+
.__test;
55+
56+
afterEach(() => {
57+
maplibreTest.workerUrlsAtConstruction.length = 0;
58+
});
59+
60+
describe("LocationMinimap", () => {
61+
it("configures the versioned worker before constructing its map", async () => {
62+
render(<LocationMinimap lng={6.084959} lat={50.780558} />);
63+
64+
await waitFor(() => expect(maplibreTest.workerUrlsAtConstruction).toHaveLength(1));
65+
expect(maplibreTest.workerUrlsAtConstruction).toEqual([
66+
"/runtime/maplibre-gl/6.1.0/maplibre-gl-worker.mjs",
67+
]);
68+
});
69+
});

apps/web/src/components/map/LocationMinimap.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { StyleSpecification } from "maplibre-gl";
66
import { useEffect, useRef } from "react";
77
import { useEnv } from "@/lib/EnvProvider";
88
import { baseMapCreditsHtml, loadMaptilerStyle, loadOpenMapXStyle } from "@/lib/map";
9+
import { loadMapLibreRuntime } from "@/lib/maplibreRuntime";
910
import { MapCredits } from "./MapCredits";
1011

1112
interface LocationMinimapProps {
@@ -38,7 +39,7 @@ export function LocationMinimap({ lng, lat, zoom = 16, onClick, sx }: LocationMi
3839

3940
(async () => {
4041
const [maplibregl, style] = await Promise.all([
41-
import("maplibre-gl"),
42+
loadMapLibreRuntime(),
4243
env.styleProvider === "openmapx"
4344
? loadOpenMapXStyle(env, variant)
4445
: loadMaptilerStyle("bright-v2", env),

apps/web/src/components/map/MapCanvas.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ vi.mock("@/lib/offlineAreas", () => ({
7373
vi.mock("maplibre-gl", () => {
7474
const instances: FakeMap[] = [];
7575
const options: Array<{ center: [number, number]; container: HTMLElement; zoom: number }> = [];
76+
const workerUrlsAtConstruction: string[] = [];
77+
let workerUrl = "";
7678
let setupError: Error | undefined;
7779
let setupErrorOnCall = 1;
7880
let onCallCount = 0;
@@ -82,6 +84,7 @@ vi.mock("maplibre-gl", () => {
8284
constructor(mapOptions: { center: [number, number]; container: HTMLElement; zoom: number }) {
8385
instances.push(this);
8486
options.push(mapOptions);
87+
workerUrlsAtConstruction.push(workerUrl);
8588
mapOptions.container.append(document.createElement("canvas"));
8689
}
8790

@@ -101,19 +104,27 @@ vi.mock("maplibre-gl", () => {
101104
__test: {
102105
instances,
103106
options,
107+
workerUrlsAtConstruction,
104108
failSetup(error: Error, onCall = 1) {
105109
setupError = error;
106110
setupErrorOnCall = onCall;
107111
},
108112
reset() {
109113
instances.length = 0;
110114
options.length = 0;
115+
workerUrlsAtConstruction.length = 0;
116+
workerUrl = "";
111117
setupError = undefined;
112118
setupErrorOnCall = 1;
113119
onCallCount = 0;
114120
},
115121
},
122+
getVersion: () => "6.1.0",
123+
getWorkerUrl: () => workerUrl,
116124
Map: FakeMap,
125+
setWorkerUrl: (url: string) => {
126+
workerUrl = url;
127+
},
117128
};
118129
});
119130

@@ -131,6 +142,7 @@ const maplibreTest = (
131142
remove: ReturnType<typeof vi.fn>;
132143
}>;
133144
options: Array<{ center: [number, number]; zoom: number }>;
145+
workerUrlsAtConstruction: string[];
134146
failSetup(error: Error, onCall?: number): void;
135147
reset(): void;
136148
};
@@ -174,6 +186,9 @@ describe("MapCanvas", () => {
174186
const { container } = render(<MapCanvas />);
175187

176188
await waitFor(() => expect(container.querySelector("canvas")).not.toBeNull());
189+
expect(maplibreTest.workerUrlsAtConstruction).toEqual([
190+
"/runtime/maplibre-gl/6.1.0/maplibre-gl-worker.mjs",
191+
]);
177192
});
178193

179194
it("applies a fast granted location only after the saved viewport map exists", async () => {

0 commit comments

Comments
 (0)