Leaflet-node brings the full Leaflet map API to Node.js so you can render maps without a browser.
Reuse the same map setup for server-side image generation, automated tests, and CI pipelines.
- Node.js >= 20
- Linux users need glibc >= 2.18 for the bundled
@napi-rs/canvas - Peer dependency:
leaflet^1.9.0
npm install leaflet-node leaflet
yarn add leaflet-node leaflet
pnpm add leaflet-node leaflet
bun add leaflet-node leafletimport L from 'leaflet-node';
const container = document.createElement('div');
container.style.width = '600px';
container.style.height = '400px';
const map = L.map(container);
map.setView([51.505538, -0.090005], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
map.setSize(600, 400);
await new Promise((resolve) => setTimeout(resolve, 1000));
await map.saveImage('map.png');Leaflet-node can run inside Jest with either the default jsdom environment for API-only tests or the node environment for full canvas rendering.
Use the default environment and mock Leaflet to avoid creating a native canvas:
// tests/setup.ts
import { jest } from '@jest/globals';
const L = require('leaflet-node');
jest.doMock('leaflet', () => L);Switch to the node environment so leaflet-node can provide its own jsdom instance and native canvas bindings:
/**
* @jest-environment node
*/
import L from 'leaflet-node';
test('render map', async () => {
const container = document.createElement('div');
const map = L.map(container);
map.setView([51.505538, -0.090005], 13);
map.setSize(512, 512);
await map.saveImage('map.png');
});Tile Loading Warnings:
You may see warnings about "dynamic import callback" or "experimental-vm-modules" when running tests with tile layers in Jest. This is a Jest limitation with VM modules and doesn't affect the actual rendering—tiles that load successfully are rendered correctly.
To suppress these warnings, you can:
- Run Jest with experimental VM modules support:
NODE_OPTIONS='--experimental-vm-modules' jest- Or configure Jest to use isolated modules:
// jest.config.js
export default {
testEnvironment: 'node',
globals: {
'ts-jest': {
isolatedModules: true
}
}
};Tip
Need a ready-to-use map in tests? leaflet-node/testing exposes createTestMap, cleanupTestMaps, waitForTiles, and waitForMapReady helpers that work in both Vitest and Jest.
import { createTestMap, waitForMapReady } from 'leaflet-node/testing';
const map = createTestMap({ width: 400, height: 300 });
await waitForMapReady(map);Maps can be exported to disk or kept in-memory:
// Save to disk with optional encoder options
await map.saveImage('map.jpeg', { format: 'jpeg', quality: 0.85 });
// Get a Buffer for further processing
const pngBuffer = await map.toBuffer('png');If you need full control of the canvas, use map.toBuffer() and write the result yourself:
import { promises as fs } from 'fs';
const buffer = await map.toBuffer('png');
await fs.writeFile('map.png', buffer);Wait for tiles (with optional progress callbacks) before exporting:
import { waitForTiles } from 'leaflet-node/testing';
const layer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
layer.addTo(map);
await waitForTiles(layer, {
timeout: 60_000,
onProgress: ({ loaded, total }) => {
console.log(`Loaded ${loaded}/${total} tiles`);
},
});Or wait for every tile layer on a map at once:
import { waitForMapReady } from 'leaflet-node/testing';
await waitForMapReady(map, {
onTileProgress: (layer, progress) => {
console.log(layer.getAttribution(), progress);
},
});📚 View the full documentation and live examples at jburnhams.github.io/leaflet-node.
Leaflet-node automatically attempts to register the bundled Noto Sans fallback fonts. In
environments where document.currentScript is unavailable (for example, Node.js test
runners without a DOM shim), you can provide an explicit path to the font assets to
avoid warning messages:
process.env.LEAFLET_NODE_FONT_BASE_PATH = '/absolute/path/to/NotoSans-Regular.ttf';
const L = await import('leaflet-node');Alternatively, you can set the base path programmatically after importing the package:
import L, { setFontAssetBasePath } from 'leaflet-node';
setFontAssetBasePath('/absolute/path/to/NotoSans-Regular.ttf');The base path can point directly to a font file or to a directory containing the
bundled NotoSans-Regular.ttf asset.
Leaflet-node honours standard proxy environment variables:
export HTTPS_PROXY=http://proxy.example.com:8080
export HTTP_PROXY=http://proxy.example.com:8080Set them before running exports to route tile downloads through your proxy.
- Reuse map instances – keep a single map around and update the view instead of creating a new
L.map()for every render. - Batch exports – kick off multiple independent exports with
Promise.all()when your server has headroom. - Manage memory – always call
map.remove()and drop references once you are done with a map so Node.js can reclaim resources.
Questions? Open an issue on GitHub.