Skip to content
Merged
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
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,7 +75,7 @@ For a decorative map, set `:interactive="false"`. The component disables input,

## Stylesheet Loading

Nuxt Scripts injects its version-pinned MapLibre stylesheet when the SDK starts loading. To bundle the stylesheet with your app instead:
Nuxt Scripts loads the stylesheet from the `maplibre-gl` package when the SDK starts loading. To control the stylesheet through Nuxt instead:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand All@@ -93,27 +93,15 @@ export default defineNuxtConfig({

## Content Security Policy

The standard MapLibre build creates a Blob worker. Its documented CSP includes `worker-src blob:`, `child-src blob:`, and `img-src data: blob:` alongside the origins used by your style.
MapLibre v6 loads its worker from a real URL served by your own build, so `worker-src blob:` is no longer required. The map still needs `img-src data: blob:` alongside the origins used by your style.

For a policy that does not allow Blob workers, self-host MapLibre's CSP build and worker:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
registry: {
maplibre: {
scriptInput: { src: '/maplibre-gl-csp.js' },
},
},
},
})
```
To serve the worker from a different path, set `worker-url`:

```vue
<ScriptMapLibreMap
:center="[144.9631, -37.8136]"
map-style="/maps/style.json"
worker-url="/maplibre-gl-csp-worker.js"
worker-url="/maplibre-gl-worker.mjs"
/>
```

Expand Down
4 changes: 2 additions & 2 deletions docs/content/scripts/maplibre/index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,14 +14,14 @@ links:

[MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) is an open-source WebGL renderer for interactive vector maps. It renders the map and handles interaction; you choose the style, tile source, attribution, geocoding, and routing services separately.

Nuxt Scripts supports MapLibre GL JS 5.24 through the [`useScriptMapLibre()`{lang="ts"}](/scripts/maplibre/api/use-script-maplibre) composable and declarative components for common map resources. First-party mode can bundle the JavaScript SDK. Nuxt Scripts loads the required MapLibre stylesheet separately by default.
Nuxt Scripts supports MapLibre GL JS v6 through the [`useScriptMapLibre()`{lang="ts"}](/scripts/maplibre/api/use-script-maplibre) composable and declarative components for common map resources. MapLibre v6 is ESM-only, so Nuxt Scripts loads it from the `maplibre-gl` package rather than a CDN script tag. The stylesheet comes from the same package.

::script-types{exclude-components}
::

## Setup

Install MapLibre for its TypeScript definitions, then enable the registry entry:
Install MapLibre v6, then enable the registry entry:

```bash
pnpm add -D maplibre-gl
Expand Down
3 changes: 2 additions & 1 deletion packages/script/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,6 +69,7 @@
"unimport",
"#nuxt-scripts/types",
"posthog-js",
"maplibre-gl",
"@nuxt/devtools-kit",
"sirv"
]
Expand All@@ -84,7 +85,7 @@
"@types/vimeo__player": "^2.18.3",
"@types/youtube": "^0.1.0",
"@unhead/vue": "^3.3.1",
"maplibre-gl": "^5.24.0",
"maplibre-gl": "^6.0.0",
"posthog-js": "^1.0.0",
"unhead": "^3.3.1"
},
Expand Down
11 changes: 5 additions & 6 deletions packages/script/src/registry-types.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -822,7 +822,7 @@
{
"name": "MapLibreOptions",
"kind": "const",
"code": "export const MapLibreOptions = object({\n /**\n * Inject the MapLibre GL JS 5.24.0 stylesheet when the script begins\n * loading. Disable this when supplying the stylesheet through Nuxt.\n * @default true\n */\n injectStyles: optional(boolean()),\n /**\n * Stylesheet URL used when `injectStyles` is enabled.\n * @default 'https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css'\n */\n stylesheetUrl: optional(string()),\n /**\n * Worker URL for the CSP-compatible MapLibre build. Pair this with a custom\n * `scriptInput.src` that loads `maplibre-gl-csp.js`.\n */\n workerUrl: optional(string()),\n})"
"code": "export const MapLibreOptions = object({\n /**\n * Inject the MapLibre stylesheet when the script begins loading. Disable\n * this when supplying the stylesheet through Nuxt.\n * @default true\n */\n injectStyles: optional(boolean()),\n /**\n * Stylesheet URL used when `injectStyles` is enabled. Defaults to the\n * stylesheet shipped in the `maplibre-gl` package.\n */\n stylesheetUrl: optional(string()),\n /**\n * Worker URL passed to `maplibregl.setWorkerUrl()`. Set this when serving\n * the MapLibre worker from your own origin.\n */\n workerUrl: optional(string()),\n})"
},
{
"name": "MapLibreApi",
Expand All@@ -837,7 +837,7 @@
{
"name": "ScriptMapLibreMapProps",
"kind": "interface",
"code": "interface ScriptMapLibreMapProps {\n /**\n * Defines when the MapLibre script loads.\n * @default 'visible'\n */\n trigger?: ElementScriptTrigger\n /** MapLibre style URL or inline style specification. */\n mapStyle: string | MapLibre.StyleSpecification\n /** Initial and reactively controlled map center. */\n center: MapLibre.LngLatLike\n /** Initial and reactively controlled zoom level. @default 12 */\n zoom?: number\n /** Initial and reactively controlled bearing in degrees. @default 0 */\n bearing?: number\n /** Initial and reactively controlled pitch in degrees. @default 0 */\n pitch?: number\n /** Options passed to `new maplibregl.Map()`. Dedicated props take precedence. */\n options?: Omit<MapLibre.MapOptions, 'container'>\n /** Inject MapLibre's stylesheet when the script begins loading. @default true */\n injectStyles?: boolean\n /** Custom MapLibre stylesheet URL. */\n stylesheetUrl?: string\n /** Worker URL used with MapLibre's CSP-compatible build. */\n workerUrl?: string\n /** Width reserved before the map loads. @default 640 */\n width?: number | string\n /** Height reserved before the map loads. @default 400 */\n height?: number | string\n /** Accessible name for an interactive map. @default 'Interactive map' */\n ariaLabel?: string\n /** Disable map input and remove it from the accessibility tree when decorative. @default true */\n interactive?: boolean\n /** Attributes applied to the outer layout container. */\n rootAttrs?: HTMLAttributes & ReservedProps & Record<string, unknown>\n}"
"code": "interface ScriptMapLibreMapProps {\n /**\n * Defines when the MapLibre script loads.\n * @default 'visible'\n */\n trigger?: ElementScriptTrigger\n /** MapLibre style URL or inline style specification. */\n mapStyle: string | MapLibre.StyleSpecification\n /** Initial and reactively controlled map center. */\n center: MapLibre.LngLatLike\n /** Initial and reactively controlled zoom level. @default 12 */\n zoom?: number\n /** Initial and reactively controlled bearing in degrees. @default 0 */\n bearing?: number\n /** Initial and reactively controlled pitch in degrees. @default 0 */\n pitch?: number\n /** Options passed to `new maplibregl.Map()`. Dedicated props take precedence. */\n options?: Omit<MapLibre.MapOptions, 'container'>\n /** Inject MapLibre's stylesheet when the script begins loading. @default true */\n injectStyles?: boolean\n /** Custom MapLibre stylesheet URL. */\n stylesheetUrl?: string\n /** Worker URL passed to `maplibregl.setWorkerUrl()`. */\n workerUrl?: string\n /** Width reserved before the map loads. @default 640 */\n width?: number | string\n /** Height reserved before the map loads. @default 400 */\n height?: number | string\n /** Accessible name for an interactive map. @default 'Interactive map' */\n ariaLabel?: string\n /** Disable map input and remove it from the accessibility tree when decorative. @default true */\n interactive?: boolean\n /** Attributes applied to the outer layout container. */\n rootAttrs?: HTMLAttributes & ReservedProps & Record<string, unknown>\n}"
},
{
"name": "ScriptMapLibreMapEvents",
Expand DownExpand Up@@ -1482,21 +1482,20 @@
"name": "injectStyles",
"type": "boolean",
"required": false,
"description": "Inject the MapLibre GL JS 5.24.0 stylesheet when the script begins loading. Disable this when supplying the stylesheet through Nuxt.",
"description": "Inject the MapLibre stylesheet when the script begins loading. Disable this when supplying the stylesheet through Nuxt.",
"defaultValue": "true"
},
{
"name": "stylesheetUrl",
"type": "string",
"required": false,
"description": "Stylesheet URL used when `injectStyles` is enabled.",
"defaultValue": "'https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css'"
"description": "Stylesheet URL used when `injectStyles` is enabled. Defaults to the stylesheet shipped in the `maplibre-gl` package."
},
{
"name": "workerUrl",
"type": "string",
"required": false,
"description": "Worker URL for the CSP-compatible MapLibre build. Pair this with a custom `scriptInput.src` that loads `maplibre-gl-csp.js`."
"description": "Worker URL passed to `maplibregl.setWorkerUrl()`. Set this when serving the MapLibre worker from your own origin."
}
],
"AhrefsAnalyticsOptions": [
Expand Down
7 changes: 4 additions & 3 deletions packages/script/src/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,7 +162,7 @@ export const registryMeta: RegistryScriptMeta[] = [
// content
m('googleMaps', 'Google Maps', 'content', 'useScriptGoogleMaps', {}, null),
m('leaflet', 'Leaflet', 'content', 'useScriptLeaflet', { bundle: true }, null),
m('maplibre', 'MapLibre GL JS', 'content', 'useScriptMapLibre', { bundle: true }, null),
m('maplibre', 'MapLibre GL JS', 'content', 'useScriptMapLibre', {}, null),
m('instagramEmbed', 'Instagram Embed', 'content', false, {}, null),
m('xEmbed', 'X Embed', 'content', false, {}, null),
m('blueskyEmbed', 'Bluesky Embed', 'content', false, {}, null),
Expand DownExpand Up@@ -708,9 +708,10 @@ export async function registry(resolve?: (path: string) => Promise<string>): Pro
composableName: 'useScriptMapLibre',
schema: MapLibreOptions,
label: 'MapLibre GL JS',
src: 'https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js',
// MapLibre GL JS v6 is ESM-only, so it loads from the `maplibre-gl`
// package rather than a CDN script tag.
src: false,
category: 'content',
bundle: true,
}),
def('blueskyEmbed', {
composableName: false,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ export interface ScriptMapLibreMapProps {
injectStyles?: boolean
/** Custom MapLibre stylesheet URL. */
stylesheetUrl?: string
/** Worker URL used with MapLibre's CSP-compatible build. */
/** Worker URL passed to `maplibregl.setWorkerUrl()`. */
workerUrl?: string
/** Width reserved before the map loads. @default 640 */
width?: number | string
Expand Down
36 changes: 24 additions & 12 deletions packages/script/src/runtime/maplibre-styles.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
export const MAPLIBRE_STYLESHEET_URL = 'https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css'
export const MAPLIBRE_STYLESHEET_INTEGRITY = 'sha384-uTttxo/aOKbdE5RlD/SPzSDoDmNvGlUYPjONi2MN/b7c9HPSvW07OIuyP7uL6jxK'

const MAPLIBRE_STYLE_ID = 'nuxt-scripts-maplibre-styles'

export function configureMapLibreWorker(maplibre: { setWorkerUrl: (value: string) => void } | undefined, workerUrl?: string): void {
if (maplibre && workerUrl)
interface MapLibreWorkerApi {
setWorkerUrl: (value: string) => void
}

/**
* Points MapLibre at its worker.
*
* MapLibre v6 resolves the worker from `import.meta.url`, which does not
* survive bundling, so the URL has to be set explicitly. `?worker&url` is
* required over plain `?url`: the worker imports its sibling
* `maplibre-gl-shared.mjs`.
*
* @see https://maplibre.org/maplibre-gl-js/docs/#installation
*/
export async function configureMapLibreWorker(maplibre: MapLibreWorkerApi | undefined, workerUrl?: string): Promise<void> {
if (!maplibre)
return
if (workerUrl) {
maplibre.setWorkerUrl(workerUrl)
return
}
const { default: bundledWorkerUrl } = await import('maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url')
maplibre.setWorkerUrl(bundledWorkerUrl)
}

/** Injects MapLibre's required control and marker stylesheet once. */
export function ensureMapLibreStyles(stylesheetUrl = MAPLIBRE_STYLESHEET_URL): void {
/** Injects a custom MapLibre control and marker stylesheet once. */
export function ensureMapLibreStyles(stylesheetUrl: string): void {
if (typeof document === 'undefined')
return

Expand All@@ -25,10 +42,5 @@ export function ensureMapLibreStyles(stylesheetUrl = MAPLIBRE_STYLESHEET_URL): v
link.id = injectedStylesheets.length ? `${MAPLIBRE_STYLE_ID}-${injectedStylesheets.length + 1}` : MAPLIBRE_STYLE_ID
link.dataset.nuxtScriptsMaplibre = '1'

if (stylesheetUrl === MAPLIBRE_STYLESHEET_URL) {
link.integrity = MAPLIBRE_STYLESHEET_INTEGRITY
link.crossOrigin = 'anonymous'
}

document.head.append(link)
}
55 changes: 37 additions & 18 deletions packages/script/src/runtime/registry/maplibre.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import type * as MapLibre from 'maplibre-gl'
import type { RegistryScriptInput } from '#nuxt-scripts/types'
import { configureMapLibreWorker, ensureMapLibreStyles, MAPLIBRE_STYLESHEET_URL } from '../maplibre-styles'
import { configureMapLibreWorker, ensureMapLibreStyles } from '../maplibre-styles'
import { useRegistryScript } from '../utils'
import { MapLibreOptions } from './schemas'

Expand All@@ -13,34 +13,53 @@ export interface MapLibreApi {
}

declare global {
interface Window extends MapLibreApi {}
interface Window {
maplibregl?: typeof MapLibre
}
}

/**
* MapLibre GL JS v6 is an ESM-only distribution. It has no UMD build, and its
* worker resolves against `import.meta.url`, so it is loaded from the
* `maplibre-gl` package instead of a CDN script tag.
*/
export function useScriptMapLibre<T extends MapLibreApi>(_options?: MapLibreInput) {
return useRegistryScript<T, typeof MapLibreOptions>('maplibre', (options, context) => {
return useRegistryScript<T, typeof MapLibreOptions>('maplibre', (options) => {
const injectStyles = options?.injectStyles !== false
const stylesheetUrl = options?.stylesheetUrl || MAPLIBRE_STYLESHEET_URL
const usesDefaultSource = !context.scriptInput?.src
const stylesheetUrl = options?.stylesheetUrl

return {
scriptInput: {
src: 'https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js',
...(usesDefaultSource
? {
integrity: 'sha384-5+cfbwT0iiub6VsQAdn6yz16nr6sDiQoHx6tm4O8OVYXHYOxcffFmCJBL0dgdvGp',
crossorigin: 'anonymous',
}
: {}),
},
clientInit: injectStyles ? () => ensureMapLibreStyles(stylesheetUrl) : undefined,
scriptMode: 'npm',
schema: import.meta.dev ? MapLibreOptions : undefined,
scriptOptions: {
use() {
const maplibregl = window.maplibregl
configureMapLibreWorker(maplibregl, options?.workerUrl)
return { maplibregl }
return import.meta.client && window.maplibregl
? { maplibregl: window.maplibregl }
: undefined
},
},
clientInit: import.meta.server
? undefined
: async (ctx) => {
const throwIfAborted = () => {
if (ctx?.signal.aborted)
throw ctx.signal.reason || new Error('Loading MapLibre was aborted')
}
throwIfAborted()

if (injectStyles) {
if (stylesheetUrl)
ensureMapLibreStyles(stylesheetUrl)
else
await import('maplibre-gl/dist/maplibre-gl.css')
}

const maplibregl = window.maplibregl || await import('maplibre-gl')
throwIfAborted()
await configureMapLibreWorker(maplibregl, options?.workerUrl)
window.maplibregl = maplibregl
return { maplibregl }
},
}
}, _options)
}
12 changes: 6 additions & 6 deletions packages/script/src/runtime/registry/schemas.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,19 +30,19 @@ export const LeafletOptions = object({

export const MapLibreOptions = object({
/**
* Inject the MapLibre GL JS 5.24.0 stylesheet when the script begins
* loading. Disable this when supplying the stylesheet through Nuxt.
* Inject the MapLibre stylesheet when the script begins loading. Disable
* this when supplying the stylesheet through Nuxt.
* @default true
*/
injectStyles: optional(boolean()),
/**
* Stylesheet URL used when `injectStyles` is enabled.
* @default 'https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css'
* Stylesheet URL used when `injectStyles` is enabled. Defaults to the
* stylesheet shipped in the `maplibre-gl` package.
*/
stylesheetUrl: optional(string()),
/**
* Worker URL for the CSP-compatible MapLibre build. Pair this with a custom
* `scriptInput.src` that loads `maplibre-gl-csp.js`.
* Worker URL passed to `maplibregl.setWorkerUrl()`. Set this when serving
* the MapLibre worker from your own origin.
*/
workerUrl: optional(string()),
})
Expand Down
3 changes: 2 additions & 1 deletion packages/script/src/script-meta.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -188,7 +188,8 @@ export const scriptMeta = {
trackedData: [],
},
maplibre: {
urls: ['https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js'],
// Loaded from the `maplibre-gl` package, not a CDN.
urls: [],
trackedData: [],
},

Expand Down
Loading
Loading