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
9 changes: 9 additions & 0 deletions docs/content/docs/4.migration-guide/1.v0-to-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

Third-party scripts expose data that enables fingerprinting users across sites. Every request shares the user's IP address, and scripts can set third-party cookies for cross-site tracking.

First-party mode acts as a reverse proxy: scripts are bundled at build time and served from your domain, while runtime collection requests are forwarded through Nitro server routes with automatic anonymisation. It is **auto-enabled** for scripts that support it, zero-config:

Check warning on line 14 in docs/content/docs/4.migration-guide/1.v0-to-v1.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "are bundled". Consider rewriting in active voice

Check warning on line 14 in docs/content/docs/4.migration-guide/1.v0-to-v1.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "are bundled". Consider rewriting in active voice

```ts
export default defineNuxtConfig({
Expand Down Expand Up @@ -220,7 +220,7 @@
})
```

If you only need infrastructure without loading the script on the page, set `trigger: false` explicitly. This registers proxy routes, TypeScript types, and bundling config, but no `<script>`{lang="html"} tag is injected. Useful when you load the script yourself via a component or composable.

Check warning on line 223 in docs/content/docs/4.migration-guide/1.v0-to-v1.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is injected". Consider rewriting in active voice

Check warning on line 223 in docs/content/docs/4.migration-guide/1.v0-to-v1.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is injected". Consider rewriting in active voice

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand Down Expand Up @@ -383,6 +383,15 @@
+</ScriptGoogleMaps>
```

#### Top-Level `center` and `zoom` Props Deprecated

The top-level `center` and `zoom` props on `<ScriptGoogleMaps>`{lang="html"} are deprecated in favour of passing them via `mapOptions`. Both APIs still work; using the legacy form emits a dev-mode warning. When both are set, `mapOptions` wins.

```diff
-<ScriptGoogleMaps :center="{ lat, lng }" :zoom="12" />
+<ScriptGoogleMaps :map-options="{ center: { lat, lng }, zoom: 12 }" />
```

### Google Maps Static Placeholder ([#673](https://github.com/nuxt/scripts/pull/673))

v1 extracts the built-in static map placeholder into a standalone [`<ScriptGoogleMapsStaticMap>`{lang="html"}](/scripts/google-maps/api/static-map) component. This removes the following props from `<ScriptGoogleMaps>`{lang="html"}:
Expand Down
14 changes: 14 additions & 0 deletions docs/content/scripts/google-maps/2.api/1.script-google-maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ By default, it will load on the `mouseenter`, `mouseover`, and `mousedown` event

See the [Facade Component API](/docs/guides/facade-components#facade-components-api) for all props, events, and slots.

::tip
**Deprecated:** the top-level `center` and `zoom` props are now deprecated. Pass them via `mapOptions` instead. The legacy props still work and emit a dev-mode warning when used. `mapOptions.center` and `mapOptions.zoom` take precedence when both are set.

```vue
<template>
<!-- Before (deprecated) -->
<ScriptGoogleMaps :center="{ lat, lng }" :zoom="12" />

<!-- After -->
<ScriptGoogleMaps :map-options="{ center: { lat, lng }, zoom: 12 }" />
</template>
```
::

## Template Ref API

Access the basic Google Maps instances via a template ref. The exposed object contains:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,20 @@ export interface ScriptGoogleMapsProps {
apiKey?: string
/**
* A latitude / longitude of where to focus the map.
*
* @deprecated Pass `center` via `mapOptions` instead. The top-level `center`
* prop will be removed in a future major version. When both are set,
* `mapOptions.center` wins.
* @see https://scripts.nuxt.com/docs/migration-guide/v0-to-v1
*/
center?: google.maps.LatLng | google.maps.LatLngLiteral | `${string},${string}`
/**
* Zoom level for the map (0-21). Reactive: changing this will update the map.
* Takes precedence over mapOptions.zoom when provided.
*
* @deprecated Pass `zoom` via `mapOptions` instead. The top-level `zoom`
* prop will be removed in a future major version. When both are set,
* `mapOptions.zoom` wins.
* @see https://scripts.nuxt.com/docs/migration-guide/v0-to-v1
*/
zoom?: number
/**
Expand Down Expand Up @@ -138,7 +147,7 @@ import { defu } from 'defu'
import { tryUseNuxtApp, useHead, useRuntimeConfig } from 'nuxt/app'
import { computed, onBeforeUnmount, onMounted, provide, ref, shallowRef, toRaw, useAttrs, useTemplateRef, watch } from 'vue'
import ScriptAriaLoadingIndicator from '../ScriptAriaLoadingIndicator.vue'
import { MAP_INJECTION_KEY, waitForMapsReady } from './useGoogleMapsResource'
import { MAP_INJECTION_KEY, waitForMapsReady, warnDeprecatedTopLevelMapProps } from './useGoogleMapsResource'

const props = withDefaults(defineProps<ScriptGoogleMapsProps>(), {
// @ts-expect-error untyped
Expand Down Expand Up @@ -184,6 +193,7 @@ if (import.meta.dev) {
if (prop in attrs)
console.warn(`[nuxt-scripts] <ScriptGoogleMaps> prop "${prop}" was removed in v1. ${message} See https://scripts.nuxt.com/docs/migration-guide/v0-to-v1`)
}
warnDeprecatedTopLevelMapProps({ center: props.center, zoom: props.zoom })
}

const rootEl = useTemplateRef<HTMLElement>('rootEl')
Expand All @@ -204,10 +214,17 @@ const { load, status, onLoaded } = useScriptGoogleMaps({

const options = computed(() => {
const mapId = props.mapOptions?.styles ? undefined : (currentMapId.value || 'map')
return defu({ center: centerOverride.value, mapId, zoom: props.zoom }, props.mapOptions, {
center: props.center,
zoom: 15,
})
// Precedence (defu merges left-to-right, leftmost wins):
// 1. centerOverride: resolved query result, always wins for center
// 2. mapOptions: preferred public API
// 3. deprecated top-level: legacy fallback for center/zoom
// 4. defaults: { zoom: 15 } when nothing else is set
return defu(
{ center: centerOverride.value, mapId },
props.mapOptions,
{ center: props.center, zoom: props.zoom },
{ zoom: 15 },
)
})
const isMapReady = ref(false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,33 @@ export interface GoogleMapsResourceContext {
mapsApi: typeof google.maps
}

/**
* Emits dev-mode deprecation warnings for the legacy top-level `center` and
* `zoom` props on `<ScriptGoogleMaps>`. Both props still work, but new code
* should pass them via `mapOptions` instead.
*
* Returns the number of warnings emitted (useful for tests).
*/
export function warnDeprecatedTopLevelMapProps(props: {
center?: unknown
zoom?: unknown
}): number {
let warned = 0
if (props.center !== undefined) {
warned++
console.warn(
'[nuxt-scripts] <ScriptGoogleMaps> prop "center" is deprecated; use `:map-options="{ center: ... }"` instead. See https://scripts.nuxt.com/docs/migration-guide/v0-to-v1',
)
}
if (props.zoom !== undefined) {
warned++
console.warn(
'[nuxt-scripts] <ScriptGoogleMaps> prop "zoom" is deprecated; use `:map-options="{ zoom: ... }"` instead. See https://scripts.nuxt.com/docs/migration-guide/v0-to-v1',
)
}
return warned
}

/**
* Wait until the Google Maps API and a Map instance are both available.
*
Expand Down
143 changes: 142 additions & 1 deletion test/unit/google-maps-components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type { MocksType } from './__helpers__/google-maps-test-utils'
/**
* @vitest-environment happy-dom
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defu } from 'defu'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { warnDeprecatedTopLevelMapProps } from '../../packages/script/src/runtime/components/GoogleMaps/useGoogleMapsResource'
import {

simulateAdvancedMarkerLifecycle,
Expand Down Expand Up @@ -366,3 +368,142 @@ describe('google Maps SFC Components Logic', () => {
})
})
})

describe('scriptGoogleMaps top-level center/zoom deprecation', () => {
// Guards the deprecation path introduced when migrating users from
// top-level `center`/`zoom` props to `mapOptions.{center,zoom}`. Both
// APIs must keep working; the new API takes precedence.

describe('warnDeprecatedTopLevelMapProps', () => {
let warnSpy: ReturnType<typeof vi.spyOn>

beforeEach(() => {
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
})

afterEach(() => {
warnSpy.mockRestore()
})

it('warns when top-level center is set', () => {
const warned = warnDeprecatedTopLevelMapProps({ center: { lat: 0, lng: 0 } })

expect(warned).toBe(1)
expect(warnSpy).toHaveBeenCalledTimes(1)
expect(warnSpy.mock.calls[0]![0]).toContain('"center" is deprecated')
expect(warnSpy.mock.calls[0]![0]).toContain('map-options')
expect(warnSpy.mock.calls[0]![0]).toContain('migration-guide/v0-to-v1')
})

it('warns when top-level zoom is set', () => {
const warned = warnDeprecatedTopLevelMapProps({ zoom: 12 })

expect(warned).toBe(1)
expect(warnSpy).toHaveBeenCalledTimes(1)
expect(warnSpy.mock.calls[0]![0]).toContain('"zoom" is deprecated')
})

it('warns once for each prop when both are set', () => {
const warned = warnDeprecatedTopLevelMapProps({ center: { lat: 0, lng: 0 }, zoom: 8 })

expect(warned).toBe(2)
expect(warnSpy).toHaveBeenCalledTimes(2)
})

it('does not warn when neither prop is set', () => {
const warned = warnDeprecatedTopLevelMapProps({})

expect(warned).toBe(0)
expect(warnSpy).not.toHaveBeenCalled()
})

it('does not warn for explicit undefined values', () => {
// withDefaults leaves undeclared optional props as undefined; that
// must not trip the warning.
const warned = warnDeprecatedTopLevelMapProps({ center: undefined, zoom: undefined })

expect(warned).toBe(0)
expect(warnSpy).not.toHaveBeenCalled()
})
})

describe('mapOptions precedence over deprecated top-level props', () => {
// Mirrors the precedence order used in ScriptGoogleMaps.vue's `options`
// computed: { centerOverride, mapId } > mapOptions > { props.center,
// props.zoom } > { zoom: 15 }. defu merges left-to-right, leftmost wins.
function mergeOptions(props: {
center?: any
zoom?: number
mapOptions?: Record<string, any>
centerOverride?: any
mapId?: string
}) {
return defu(
{ center: props.centerOverride, mapId: props.mapId },
props.mapOptions,
{ center: props.center, zoom: props.zoom },
{ zoom: 15 },
)
}

it('mapOptions.center wins over deprecated top-level center', () => {
const merged = mergeOptions({
center: { lat: 1, lng: 1 },
mapOptions: { center: { lat: 2, lng: 2 } },
})

expect(merged.center).toEqual({ lat: 2, lng: 2 })
})

it('mapOptions.zoom wins over deprecated top-level zoom', () => {
const merged = mergeOptions({
zoom: 5,
mapOptions: { zoom: 10 },
})

expect(merged.zoom).toBe(10)
})

it('top-level center is used when mapOptions.center is absent', () => {
const merged = mergeOptions({
center: { lat: 3, lng: 4 },
mapOptions: { mapTypeId: 'roadmap' },
})

expect(merged.center).toEqual({ lat: 3, lng: 4 })
expect(merged.mapTypeId).toBe('roadmap')
})

it('top-level zoom is used when mapOptions.zoom is absent', () => {
const merged = mergeOptions({
zoom: 7,
mapOptions: { mapTypeId: 'satellite' },
})

expect(merged.zoom).toBe(7)
})

it('produces identical merged options for old and new APIs', () => {
const oldApi = mergeOptions({ center: { lat: 5, lng: 6 }, zoom: 14 })
const newApi = mergeOptions({ mapOptions: { center: { lat: 5, lng: 6 }, zoom: 14 } })

expect(oldApi).toEqual(newApi)
})

it('falls back to default zoom when nothing is set', () => {
const merged = mergeOptions({})

expect(merged.zoom).toBe(15)
})

it('centerOverride (resolved query result) wins over both APIs', () => {
const merged = mergeOptions({
center: { lat: 1, lng: 1 },
mapOptions: { center: { lat: 2, lng: 2 } },
centerOverride: { lat: 99, lng: 99 },
})

expect(merged.center).toEqual({ lat: 99, lng: 99 })
})
})
})
Loading