From bd5136e994f917b116920800feb96ca67c269923 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 9 Apr 2026 23:18:43 +1000 Subject: [PATCH] refactor(google-maps)!: deprecate top-level center/zoom props Mark ``'s top-level `center` and `zoom` props as deprecated in favour of passing them through `mapOptions`. Both APIs keep working; the legacy form emits a dev-mode console warning. Behaviour changes: - `mapOptions.center` and `mapOptions.zoom` now take precedence over the deprecated top-level props when both are set. Previously the top-level props won (the JSDoc on `zoom` claimed precedence; defu ordering matched). The new ordering is: resolved query > mapOptions > deprecated top-level > defaults. - The `centerOverride` mechanism for resolved location queries still wins over both APIs. - The skip-if-equal `setCenter` watcher and the `setOptions` exclusion of zoom/center are untouched. Extracted `warnDeprecatedTopLevelMapProps` into `useGoogleMapsResource` so the dev-warning behaviour is unit-testable without mounting the SFC. Docs updated: deprecation tip on the API page and a v0->v1 migration section showing the before/after snippet. --- .../docs/4.migration-guide/1.v0-to-v1.md | 9 ++ .../google-maps/2.api/1.script-google-maps.md | 14 ++ .../GoogleMaps/ScriptGoogleMaps.vue | 29 +++- .../GoogleMaps/useGoogleMapsResource.ts | 27 ++++ test/unit/google-maps-components.test.ts | 143 +++++++++++++++++- 5 files changed, 215 insertions(+), 7 deletions(-) diff --git a/docs/content/docs/4.migration-guide/1.v0-to-v1.md b/docs/content/docs/4.migration-guide/1.v0-to-v1.md index 69f7c8e6..b9b4ddd2 100644 --- a/docs/content/docs/4.migration-guide/1.v0-to-v1.md +++ b/docs/content/docs/4.migration-guide/1.v0-to-v1.md @@ -383,6 +383,15 @@ Use child `ScriptGoogleMapsMarker` components instead: + ``` +#### Top-Level `center` and `zoom` Props Deprecated + +The top-level `center` and `zoom` props on ``{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 +- ++ +``` + ### Google Maps Static Placeholder ([#673](https://github.com/nuxt/scripts/pull/673)) v1 extracts the built-in static map placeholder into a standalone [``{lang="html"}](/scripts/google-maps/api/static-map) component. This removes the following props from ``{lang="html"}: diff --git a/docs/content/scripts/google-maps/2.api/1.script-google-maps.md b/docs/content/scripts/google-maps/2.api/1.script-google-maps.md index bdbc67f0..f5febe1a 100644 --- a/docs/content/scripts/google-maps/2.api/1.script-google-maps.md +++ b/docs/content/scripts/google-maps/2.api/1.script-google-maps.md @@ -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 Ref API Access the basic Google Maps instances via a template ref. The exposed object contains: diff --git a/packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue b/packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue index d0db9b3d..883c9ff7 100644 --- a/packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +++ b/packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue @@ -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 /** @@ -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(), { // @ts-expect-error untyped @@ -184,6 +193,7 @@ if (import.meta.dev) { if (prop in attrs) console.warn(`[nuxt-scripts] 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('rootEl') @@ -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) diff --git a/packages/script/src/runtime/components/GoogleMaps/useGoogleMapsResource.ts b/packages/script/src/runtime/components/GoogleMaps/useGoogleMapsResource.ts index 3a302998..f8193fa0 100644 --- a/packages/script/src/runtime/components/GoogleMaps/useGoogleMapsResource.ts +++ b/packages/script/src/runtime/components/GoogleMaps/useGoogleMapsResource.ts @@ -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 ``. 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] 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] 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. * diff --git a/test/unit/google-maps-components.test.ts b/test/unit/google-maps-components.test.ts index 8a3b3eb9..43c9647c 100644 --- a/test/unit/google-maps-components.test.ts +++ b/test/unit/google-maps-components.test.ts @@ -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, @@ -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 + + 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 + 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 }) + }) + }) +})