Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## main

### ✨ Features and improvements
- ESM bundle ([#6254](https://github.com/maplibre/maplibre-gl-js/pull/6254))
- _...Add new stuff here..._

### 🐞 Bug fixes
Expand Down
Binary file added docs/assets/examples/display-a-map-with-esm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
"version": "5.7.2",
"main": "dist/maplibre-gl.js",
"module": "dist/maplibre-gl.mjs",
"style": "dist/maplibre-gl.css",
"license": "BSD-3-Clause",
"homepage": "https://maplibre.org/",
Expand Down
9 changes: 7 additions & 2 deletions rollup.config.csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const {BUILD} = process.env;
const production: boolean = (BUILD !== 'dev');
const outputPostfix: string = production ? '' : '-dev';

const config = (input: InputOption, file: string, format: ModuleFormat): RollupOptions => ({
export const config = (input: InputOption, file: string, format: ModuleFormat): RollupOptions => ({
input,
output: {
name: 'maplibregl',
Expand All @@ -25,8 +25,13 @@ const config = (input: InputOption, file: string, format: ModuleFormat): RollupO
});

const configs = [
// UMD/IIFE builds for CSP
config('src/index.ts', `dist/maplibre-gl-csp${outputPostfix}.js`, 'umd'),
config('src/source/worker.ts', `dist/maplibre-gl-csp-worker${outputPostfix}.js`, 'iife')
config('src/source/worker.ts', `dist/maplibre-gl-csp-worker${outputPostfix}.js`, 'iife'),

// ESM builds for CSP
config('src/index.ts', `dist/maplibre-gl-csp${outputPostfix}.mjs`, 'es'),
config('src/source/worker.ts', `dist/maplibre-gl-csp-worker${outputPostfix}.mjs`, 'es')
];

export default configs;
10 changes: 9 additions & 1 deletion rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import sourcemaps from 'rollup-plugin-sourcemaps2';
import {plugins, watchStagingPlugin} from './build/rollup_plugins';
import banner from './build/banner';
import {type RollupOptions} from 'rollup';
import {config as cspConfig} from './rollup.config.csp';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a better name to the file and method is probably needed here.


const {BUILD} = process.env;

const production = BUILD === 'production';
const outputFile = production ? 'dist/maplibre-gl.js' : 'dist/maplibre-gl-dev.js';
const outputPostfix: string = production ? '' : '-dev';

const config: RollupOptions[] = [{
// Rollup will use code splitting to bundle GL JS into three "chunks":
Expand Down Expand Up @@ -62,6 +64,12 @@ const config: RollupOptions[] = [{
// only they get built, but not the merged dev build js
...production ? [] : [watchStagingPlugin]
],
}];
},

// ESM builds
cspConfig('src/index.ts', `dist/maplibre-gl${outputPostfix}.mjs`, 'es'),
cspConfig('src/source/worker.ts', `dist/maplibre-gl-worker${outputPostfix}.mjs`, 'es'),

];

export default config;
12 changes: 12 additions & 0 deletions src/util/web_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,17 @@ export interface WorkerGlobalScopeInterface {
}

export function workerFactory() {
// Check if we should use module workers (for ESM builds)
const useModuleWorker = config.WORKER_URL && config.WORKER_URL.endsWith('.mjs');

if (useModuleWorker) {
try {
return new Worker(config.WORKER_URL, {type: 'module'});
} catch (e) {
// Fallback to regular worker if module workers not supported
console.warn('Module worker not supported, falling back to classic worker', e);
}
}

return new Worker(config.WORKER_URL);
}
75 changes: 75 additions & 0 deletions test/build/esm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {describe, test, expect} from 'vitest';
import fs from 'fs';
import path from 'path';

describe('ESM build', () => {
test('ESM main bundle exists and exports expected API', () => {
const esmPath = path.join(process.cwd(), 'dist/maplibre-gl-dev.mjs');
expect(fs.existsSync(esmPath)).toBe(true);

const content = fs.readFileSync(esmPath, 'utf8');

// Check for ES module exports at the end of the file
expect(content).toMatch(/export\s+\{[^}]+\};\s*$/m);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super thrilled with checking the string content of the file, is there a better way to check this?

expect(content).toContain('Map');
expect(content).toContain('Marker');
expect(content).toContain('Popup');
expect(content).toContain('setWorkerUrl');
expect(content).toContain('getWorkerUrl');

// The bundle should use ES module format (export statements)
// Note: Some dependencies may contain module.exports internally,
// but the bundle itself should export using ES module syntax
expect(content).toMatch(/^export\s+\{/m);
});

test('ESM worker bundle exists', () => {
const workerPath = path.join(process.cwd(), 'dist/maplibre-gl-worker-dev.mjs');
expect(fs.existsSync(workerPath)).toBe(true);

const content = fs.readFileSync(workerPath, 'utf8');

// Worker should be an ES module
// The worker doesn't export anything but should not use AMD
expect(content).not.toContain('define.amd');

// Should contain worker-specific code
expect(content).toContain('Actor');
});

test('Production ESM builds exist', () => {
// These might not exist if only dev build was run
const mainProd = path.join(process.cwd(), 'dist/maplibre-gl.mjs');
const workerProd = path.join(process.cwd(), 'dist/maplibre-gl-worker.mjs');

if (fs.existsSync(mainProd)) {
const content = fs.readFileSync(mainProd, 'utf8');
// Production build is minified, just check for export statement
expect(content).toContain('export');
}

if (fs.existsSync(workerProd)) {
const content = fs.readFileSync(workerProd, 'utf8');
// Should be ES module format with export statement
expect(content).toContain('export');
}
});

test('CSP ESM builds follow same pattern', () => {
const cspMain = path.join(process.cwd(), 'dist/maplibre-gl-csp-dev.mjs');
const cspWorker = path.join(process.cwd(), 'dist/maplibre-gl-csp-worker-dev.mjs');

if (fs.existsSync(cspMain)) {
const content = fs.readFileSync(cspMain, 'utf8');
expect(content).toContain('export {');
// Should be ES module format with export statement
expect(content).toContain('export');
}

if (fs.existsSync(cspWorker)) {
const content = fs.readFileSync(cspWorker, 'utf8');
// Should be ES module format with export statement
expect(content).toContain('export');
}
});
});
2 changes: 1 addition & 1 deletion test/build/sourcemaps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ describe('main sourcemap', () => {
const s1 = setMinus(actualEntriesInSourcemapJSON, expectedEntriesInSourcemapJSON);
expect(s1.length).toBeLessThan(5);
const s2 = setMinus(expectedEntriesInSourcemapJSON, actualEntriesInSourcemapJSON);
expect(s2.length).toBeLessThan(16);
expect(s2.length).toBeLessThan(28);
});
});
33 changes: 33 additions & 0 deletions test/examples/display-a-map-with-esm.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Display a map with ESM</title>
<meta property="og:description" content="Initialize a map in an HTML element with MapLibre GL JS using EcmaScript Module (ESM)." />
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel='stylesheet' href='../../dist/maplibre-gl.css' />
</head>
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script type="module">
import * as maplibregl from '../../dist/maplibre-gl.mjs';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this will work in the docs' examples.
Can you check if a change is needed around here:

htmlContent = htmlContent.replace(/-dev.js/g, '.js');


// Set the worker URL for ESM build (CSP-style pattern)
maplibregl.setWorkerUrl('../../dist/maplibre-gl-worker-dev.mjs');


const map = new maplibregl.Map({
container: 'map', // container id
style: 'https://demotiles.maplibre.org/style.json', // style URL
center: [0, 0], // starting position [lng, lat]
zoom: 1, // starting zoom
maplibreLogo: true
});
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion test/examples/filter-layer-symbols-using-global-state.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,4 @@
});
</script>

</html>
</html>
Loading