-
Notifications
You must be signed in to change notification settings - Fork 85
refactor(google-maps): export typed interfaces, internal cleanups #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,26 +2,13 @@ | |||||||||||||||||||||||||||
| /// <reference types="google.maps" /> | ||||||||||||||||||||||||||||
| import type { ElementScriptTrigger } from '#nuxt-scripts/types' | ||||||||||||||||||||||||||||
| import type { HTMLAttributes, ReservedProps, ShallowRef } from 'vue' | ||||||||||||||||||||||||||||
| import { useScriptTriggerElement } from '#nuxt-scripts/composables/useScriptTriggerElement' | ||||||||||||||||||||||||||||
| import { useScriptGoogleMaps } from '#nuxt-scripts/registry/google-maps' | ||||||||||||||||||||||||||||
| import { scriptRuntimeConfig, scriptsPrefix } from '#nuxt-scripts/utils' | ||||||||||||||||||||||||||||
| import { defu } from 'defu' | ||||||||||||||||||||||||||||
| import { tryUseNuxtApp, useHead, useRuntimeConfig } from 'nuxt/app' | ||||||||||||||||||||||||||||
| import { computed, onBeforeUnmount, onMounted, provide, ref, shallowRef, toRaw, useAttrs, watch } from 'vue' | ||||||||||||||||||||||||||||
| import ScriptAriaLoadingIndicator from '../ScriptAriaLoadingIndicator.vue' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import { MAP_INJECTION_KEY } from './useGoogleMapsResource' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const DIGITS_ONLY_RE = /^\d+$/ | ||||||||||||||||||||||||||||
| const DIGITS_PX_RE = /^\d+px$/i | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export { MAP_INJECTION_KEY } from './useGoogleMapsResource' | ||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| <script lang="ts" setup> | ||||||||||||||||||||||||||||
| const props = withDefaults(defineProps<{ | ||||||||||||||||||||||||||||
| export interface ScriptGoogleMapsProps { | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Defines the trigger event to load the script. | ||||||||||||||||||||||||||||
| * @default ['mouseenter', 'mouseover', 'mousedown'] | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| trigger?: ElementScriptTrigger | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
|
|
@@ -46,19 +33,21 @@ const props = withDefaults(defineProps<{ | |||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| region?: string | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Defines the language of the map | ||||||||||||||||||||||||||||
| * Defines the language of the map. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| language?: string | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Defines the version of google maps js API | ||||||||||||||||||||||||||||
| * Defines the version of google maps js API. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| version?: string | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Defines the width of the map. | ||||||||||||||||||||||||||||
| * @default 640 | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| width?: number | string | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Defines the height of the map | ||||||||||||||||||||||||||||
| * Defines the height of the map. | ||||||||||||||||||||||||||||
| * @default 400 | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| height?: number | string | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
|
|
@@ -73,57 +62,112 @@ const props = withDefaults(defineProps<{ | |||||||||||||||||||||||||||
| mapIds?: { light?: string, dark?: string } | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Manual color mode control. When provided, overrides auto-detection from @nuxtjs/color-mode. | ||||||||||||||||||||||||||||
| * Accepts 'light', 'dark', or a reactive ref. | ||||||||||||||||||||||||||||
| * Accepts 'light' or 'dark'. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| colorMode?: 'light' | 'dark' | ||||||||||||||||||||||||||||
| }>(), { | ||||||||||||||||||||||||||||
| // @ts-expect-error untyped | ||||||||||||||||||||||||||||
| trigger: ['mouseenter', 'mouseover', 'mousedown'], | ||||||||||||||||||||||||||||
| width: 640, | ||||||||||||||||||||||||||||
| height: 400, | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export interface ScriptGoogleMapsExpose { | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * A reference to the loaded Google Maps API, or `undefined` if not yet loaded. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| googleMaps: ShallowRef<typeof google.maps | undefined> | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * A reference to the Google Map instance, or `undefined` if not yet initialized. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| map: ShallowRef<google.maps.Map | undefined> | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Utility function to resolve a location query (e.g. "New York, NY") to latitude/longitude coordinates. | ||||||||||||||||||||||||||||
| * Uses a caching mechanism and a server-side proxy to avoid unnecessary client-side API calls. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| resolveQueryToLatLng: (query: string) => Promise<google.maps.LatLng | google.maps.LatLngLiteral | undefined> | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Utility function to dynamically import additional Google Maps libraries (e.g. "marker", "places"). | ||||||||||||||||||||||||||||
| * Caches imported libraries for efficient reuse. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| importLibrary: { | ||||||||||||||||||||||||||||
| (key: 'marker'): Promise<google.maps.MarkerLibrary> | ||||||||||||||||||||||||||||
| (key: 'places'): Promise<google.maps.PlacesLibrary> | ||||||||||||||||||||||||||||
| (key: 'geometry'): Promise<google.maps.GeometryLibrary> | ||||||||||||||||||||||||||||
| (key: 'drawing'): Promise<google.maps.DrawingLibrary> | ||||||||||||||||||||||||||||
| (key: 'visualization'): Promise<google.maps.VisualizationLibrary> | ||||||||||||||||||||||||||||
| (key: string): Promise<any> | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const emits = defineEmits<{ | ||||||||||||||||||||||||||||
| export interface ScriptGoogleMapsEmits { | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Fired when the Google Maps instance is fully loaded and ready to use. Provides access to the maps API. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| ready: [e: typeof googleMaps] | ||||||||||||||||||||||||||||
| ready: [payload: ScriptGoogleMapsExpose] | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Fired when the Google Maps script fails to load. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| error: [] | ||||||||||||||||||||||||||||
| }>() | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| defineSlots<{ | ||||||||||||||||||||||||||||
| export interface ScriptGoogleMapsSlots { | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Default slot for rendering child components (e.g. markers, info windows) that depend on the map being ready. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| default?: () => any | ||||||||||||||||||||||||||||
| placeholder?: () => any | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Slot displayed while the map is loading. Can be used to show a custom loading indicator. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| loading?: () => any | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Slot displayed when the script is awaiting user interaction to load (based on the `trigger` prop). | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| awaitingLoad?: () => any | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Slot displayed if the script fails to load. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| error?: () => any | ||||||||||||||||||||||||||||
| }>() | ||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Slot displayed as a placeholder before the map is ready. Useful for showing a static map or skeleton. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| placeholder?: () => any | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| <script lang="ts" setup> | ||||||||||||||||||||||||||||
| import { useScriptTriggerElement } from '#nuxt-scripts/composables/useScriptTriggerElement' | ||||||||||||||||||||||||||||
| import { useScriptGoogleMaps } from '#nuxt-scripts/registry/google-maps' | ||||||||||||||||||||||||||||
| import { scriptRuntimeConfig, scriptsPrefix } from '#nuxt-scripts/utils' | ||||||||||||||||||||||||||||
| 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 } from './useGoogleMapsResource' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const props = withDefaults(defineProps<ScriptGoogleMapsProps>(), { | ||||||||||||||||||||||||||||
| // @ts-expect-error untyped | ||||||||||||||||||||||||||||
| trigger: ['mouseenter', 'mouseover', 'mousedown'], | ||||||||||||||||||||||||||||
| width: 640, | ||||||||||||||||||||||||||||
| height: 400, | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| const emits = defineEmits<ScriptGoogleMapsEmits>() | ||||||||||||||||||||||||||||
| defineSlots<ScriptGoogleMapsSlots>() | ||||||||||||||||||||||||||||
| const DIGITS_ONLY_RE = /^\d+$/ | ||||||||||||||||||||||||||||
| const DIGITS_PX_RE = /^\d+px$/i | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const apiKey = props.apiKey || scriptRuntimeConfig('googleMaps')?.apiKey | ||||||||||||||||||||||||||||
| const runtimeConfig = useRuntimeConfig() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Color mode support - try to auto-detect from @nuxtjs/color-mode | ||||||||||||||||||||||||||||
| const nuxtApp = tryUseNuxtApp() | ||||||||||||||||||||||||||||
| const nuxtColorMode = nuxtApp?.$colorMode as { value: string } | undefined | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const currentColorMode = computed(() => { | ||||||||||||||||||||||||||||
| if (props.colorMode) | ||||||||||||||||||||||||||||
| return props.colorMode | ||||||||||||||||||||||||||||
| if (nuxtColorMode?.value) | ||||||||||||||||||||||||||||
| return nuxtColorMode.value === 'dark' ? 'dark' : 'light' | ||||||||||||||||||||||||||||
| return 'light' | ||||||||||||||||||||||||||||
| const nuxtColorMode = computed(() => { | ||||||||||||||||||||||||||||
| const value = (tryUseNuxtApp()?.$colorMode as { value: string } | undefined)?.value | ||||||||||||||||||||||||||||
| return value === 'dark' || value === 'light' ? value : undefined | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const currentColorMode = computed(() => props.colorMode || nuxtColorMode.value || 'light') | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const currentMapId = computed(() => { | ||||||||||||||||||||||||||||
| if (!props.mapIds) | ||||||||||||||||||||||||||||
| return props.mapOptions?.mapId | ||||||||||||||||||||||||||||
| return props.mapIds[currentColorMode.value] || props.mapIds.light || props.mapOptions?.mapId | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const mapsApi = ref<typeof google.maps | undefined>() | ||||||||||||||||||||||||||||
| const mapsApi = shallowRef<typeof google.maps | undefined>() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (import.meta.dev) { | ||||||||||||||||||||||||||||
| if (!apiKey) | ||||||||||||||||||||||||||||
|
|
@@ -142,8 +186,8 @@ if (import.meta.dev) { | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const rootEl = ref<HTMLElement>() | ||||||||||||||||||||||||||||
| const mapEl = ref<HTMLElement>() | ||||||||||||||||||||||||||||
| const rootEl = useTemplateRef<HTMLElement>('rootEl') | ||||||||||||||||||||||||||||
| const mapEl = useTemplateRef<HTMLElement>('mapEl') | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const centerOverride = ref() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -165,7 +209,7 @@ const options = computed(() => { | |||||||||||||||||||||||||||
| zoom: 15, | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| const ready = ref(false) | ||||||||||||||||||||||||||||
| const isMapReady = ref(false) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const map: ShallowRef<google.maps.Map | undefined> = shallowRef() | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -198,31 +242,37 @@ async function resolveQueryToLatLng(query: string) { | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Fallback: use Places API client-side | ||||||||||||||||||||||||||||
| // eslint-disable-next-line no-async-promise-executor | ||||||||||||||||||||||||||||
| return new Promise<google.maps.LatLng>(async (resolve, reject) => { | ||||||||||||||||||||||||||||
| if (!mapsApi.value) { | ||||||||||||||||||||||||||||
| await load() | ||||||||||||||||||||||||||||
| // await new promise, watch until mapsApi is set | ||||||||||||||||||||||||||||
| await new Promise<void>((resolve) => { | ||||||||||||||||||||||||||||
| const _ = watch(mapsApi, () => { | ||||||||||||||||||||||||||||
| _() | ||||||||||||||||||||||||||||
| resolve() | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| if (!mapsApi.value) { | ||||||||||||||||||||||||||||
| await load() | ||||||||||||||||||||||||||||
| // await new promise, watch until mapsApi is set | ||||||||||||||||||||||||||||
| await new Promise<void>((resolve) => { | ||||||||||||||||||||||||||||
| const _ = watch(mapsApi, () => { | ||||||||||||||||||||||||||||
| _() | ||||||||||||||||||||||||||||
| resolve() | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| const placesService = new mapsApi.value!.places.PlacesService(map.value!) | ||||||||||||||||||||||||||||
| placesService.findPlaceFromQuery({ | ||||||||||||||||||||||||||||
| query, | ||||||||||||||||||||||||||||
| fields: ['name', 'geometry'], | ||||||||||||||||||||||||||||
| }, (results, status) => { | ||||||||||||||||||||||||||||
| if (status === 'OK' && results?.[0]?.geometry?.location) | ||||||||||||||||||||||||||||
| return resolve(results[0].geometry.location) | ||||||||||||||||||||||||||||
| return reject(new Error(`No location found for ${query}`)) | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| }).then((res) => { | ||||||||||||||||||||||||||||
| queryToLatLngCache.set(query, res) | ||||||||||||||||||||||||||||
| return res | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const placesService = new mapsApi.value!.places.PlacesService(map.value!) | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential null reference:
Suggested fix: wait for map initialization- const placesService = new mapsApi.value!.places.PlacesService(map.value!)
+ // Wait for map to be initialized if not already
+ if (!map.value) {
+ await new Promise<void>((resolve) => {
+ const stop = watch(map, (m) => {
+ if (m) {
+ stop()
+ resolve()
+ }
+ }, { immediate: true })
+ })
+ }
+ const placesService = new mapsApi.value!.places.PlacesService(map.value!)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pre-existing on main — same There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to open a GitHub issue to track both of these (the |
||||||||||||||||||||||||||||
| const result = await new Promise<google.maps.LatLng>((resolve, reject) => { | ||||||||||||||||||||||||||||
| placesService.findPlaceFromQuery( | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| query, | ||||||||||||||||||||||||||||
| fields: ['name', 'geometry'], | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| (results, status) => { | ||||||||||||||||||||||||||||
| if (status === 'OK' && results?.[0]?.geometry?.location) { | ||||||||||||||||||||||||||||
| resolve(results[0].geometry.location) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| else { | ||||||||||||||||||||||||||||
| reject(new Error(`No location found for ${query}`)) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| queryToLatLngCache.set(query, result) | ||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const libraries = new Map<string, any>() | ||||||||||||||||||||||||||||
|
|
@@ -254,14 +304,14 @@ function importLibrary<T>(key: string): Promise<T> { | |||||||||||||||||||||||||||
| return cached as Promise<T> | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const googleMaps = { | ||||||||||||||||||||||||||||
| const googleMaps: ScriptGoogleMapsExpose = { | ||||||||||||||||||||||||||||
| googleMaps: mapsApi, | ||||||||||||||||||||||||||||
| map, | ||||||||||||||||||||||||||||
| resolveQueryToLatLng, | ||||||||||||||||||||||||||||
| importLibrary, | ||||||||||||||||||||||||||||
| } as const | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| defineExpose(googleMaps) | ||||||||||||||||||||||||||||
| defineExpose<ScriptGoogleMapsExpose>(googleMaps) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Shared InfoWindow group: only one InfoWindow open at a time within this map | ||||||||||||||||||||||||||||
| let activeInfoWindow: google.maps.InfoWindow | undefined | ||||||||||||||||||||||||||||
|
|
@@ -277,7 +327,7 @@ provide(MAP_INJECTION_KEY, { | |||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| onMounted(() => { | ||||||||||||||||||||||||||||
| watch(ready, (v) => { | ||||||||||||||||||||||||||||
| watch(isMapReady, (v) => { | ||||||||||||||||||||||||||||
| if (v) { | ||||||||||||||||||||||||||||
| emits('ready', googleMaps) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
@@ -299,13 +349,13 @@ onMounted(() => { | |||||||||||||||||||||||||||
| if (map.value && zoom != null) | ||||||||||||||||||||||||||||
| map.value.setZoom(zoom) | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| watch([() => options.value.center, ready, map], async (next) => { | ||||||||||||||||||||||||||||
| watch([() => options.value.center, isMapReady, map], async (next) => { | ||||||||||||||||||||||||||||
| if (!map.value) { | ||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| let center = toRaw(next[0]) | ||||||||||||||||||||||||||||
| if (center) { | ||||||||||||||||||||||||||||
| if (isLocationQuery(center) && ready.value) { | ||||||||||||||||||||||||||||
| if (isLocationQuery(center) && isMapReady.value) { | ||||||||||||||||||||||||||||
| center = await resolveQueryToLatLng(center as string) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| // Skip setCenter if the map is already at the same position to avoid | ||||||||||||||||||||||||||||
|
|
@@ -334,9 +384,10 @@ onMounted(() => { | |||||||||||||||||||||||||||
| map.value = new mapsApi.value!.Map(mapEl.value!, _options) | ||||||||||||||||||||||||||||
| if (center && isLocationQuery(center)) { | ||||||||||||||||||||||||||||
| centerOverride.value = await resolveQueryToLatLng(center) | ||||||||||||||||||||||||||||
| map.value?.setCenter(centerOverride.value) | ||||||||||||||||||||||||||||
| if (centerOverride.value) | ||||||||||||||||||||||||||||
| map.value?.setCenter(centerOverride.value) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| ready.value = true | ||||||||||||||||||||||||||||
| isMapReady.value = true | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -404,9 +455,9 @@ onBeforeUnmount(() => { | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||
| <div ref="rootEl" v-bind="rootAttrs"> | ||||||||||||||||||||||||||||
| <div v-show="ready" ref="mapEl" :style="{ width: '100%', height: '100%', maxWidth: '100%' }" /> | ||||||||||||||||||||||||||||
| <slot v-if="!ready" name="placeholder" /> | ||||||||||||||||||||||||||||
| <slot v-if="status !== 'awaitingLoad' && !ready" name="loading"> | ||||||||||||||||||||||||||||
| <div v-show="isMapReady" ref="mapEl" :style="{ width: '100%', height: '100%', maxWidth: '100%' }" /> | ||||||||||||||||||||||||||||
| <slot v-if="!isMapReady" name="placeholder" /> | ||||||||||||||||||||||||||||
| <slot v-if="status !== 'awaitingLoad' && !isMapReady" name="loading"> | ||||||||||||||||||||||||||||
| <ScriptAriaLoadingIndicator /> | ||||||||||||||||||||||||||||
| </slot> | ||||||||||||||||||||||||||||
| <slot v-if="status === 'awaitingLoad'" name="awaitingLoad" /> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file structure and locate the file fd ScriptGoogleMaps.vueRepository: nuxt/scripts
Length of output: 126
🏁 Script executed:
# Read the file to understand the context around lines 246-255 wc -l packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vueRepository: nuxt/scripts
Length of output: 130
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 1823
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 38
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 3155
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 144
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 38
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 95
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 3997
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 318
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 494
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 1193
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 4822
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 38
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 907
🏁 Script executed:
# Look for useScriptGoogleMaps implementation fd useScriptGoogleMapsRepository: nuxt/scripts
Length of output: 38
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 2016
🏁 Script executed:
# Let's verify the exact timing by checking if there's synchronous behavior in useScriptGoogleMaps fd useScriptGoogleMaps -type fRepository: nuxt/scripts
Length of output: 226
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 150
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 528
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 57
🏁 Script executed:
# Find the useScriptGoogleMaps file fd useScriptGoogleMapsRepository: nuxt/scripts
Length of output: 38
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 656
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 198
🏁 Script executed:
# Get the full implementation of useScriptGoogleMaps cat packages/script/src/runtime/registry/google-maps.tsRepository: nuxt/scripts
Length of output: 2136
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 1687
🏁 Script executed:
Repository: nuxt/scripts
Length of output: 112
Close the
load()/ watcher race condition inresolveQueryToLatLng().If
load()completes and triggersonLoadedbefore the watcher at line 250 is created,mapsApi.valuewill already be set. Since the watcher is non-immediate, it only fires on value changes, not on existing values—causing it to never fire and the Promise to hang indefinitely.Recheck
mapsApi.valueafterawait load(), or use an immediate watcher with error handling via thestatusvariable:Suggested fix
if (!mapsApi.value) { await load() - // await new promise, watch until mapsApi is set - await new Promise<void>((resolve) => { - const _ = watch(mapsApi, () => { - _() - resolve() - }) - }) + if (!mapsApi.value) { + await new Promise<void>((resolve, reject) => { + const stop = watch([mapsApi, status], ([api, nextStatus]) => { + if (api) { + stop() + resolve() + } + else if (nextStatus === 'error') { + stop() + reject(new Error('Failed to load Google Maps')) + } + }, { immediate: true }) + }) + } }📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pre-existing on main — the watcher race in
resolveQueryToLatLngis identical to the version this PR replaces (same non-immediatewatch(mapsApi, ...)pattern). Out of scope for this internal-cleanup PR; will get its ownfix(google-maps): close races in resolveQueryToLatLngfollow-up alongside themap.valueguard you flagged below.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@harlan-zw, understood — the non-immediatewatch(mapsApi, ...)pattern is identical to what was onmainbefore this PR, so it's not a regression introduced here. I'll treat this as pre-existing and out of scope for this cleanup PR.Would you like me to open a GitHub issue to track the
fix(google-maps): close races in resolveQueryToLatLngfollow-up (covering both themapsApiwatcher race and themap.valueguard)?