Skip to content

refactor(google-maps): export typed interfaces, internal cleanups#692

Merged
harlan-zw merged 3 commits intomainfrom
refactor/google-maps-internal-cleanup
Apr 9, 2026
Merged

refactor(google-maps): export typed interfaces, internal cleanups#692
harlan-zw merged 3 commits intomainfrom
refactor/google-maps-internal-cleanup

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Splits the safe, additive portion out of #689 so it can land first while the breaking changes get their own targeted PRs.

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

#689 bundles a lot of unrelated work into one diff. This PR extracts the changes that have no public API impact so reviewing the breaking pieces becomes simpler.

ScriptGoogleMaps

  • Export ScriptGoogleMapsProps, …Emits, …Slots, …Expose from a non-setup script block so consumers can type their template refs and event handlers.
  • useTemplateRef for rootEl / mapEl (latest Vue API).
  • mapsApi switched from ref to shallowRef (correct for non-deeply-reactive data).
  • Internal ready ref renamed to isMapReady for clearer intent.
  • resolveQueryToLatLng cache normalisation: callers now always get a consistent LatLngLiteral shape on cache hits.
  • Color-mode handling collapsed into a single computed.

ScriptGoogleMapsOverlayView

  • Export ScriptGoogleMapsOverlayAnchor, ScriptGoogleMapsOverlayPane, and the …OverlayViewProps/Emits/Slots/Expose interfaces.
  • Marker-position normalisation refactored to use instanceof google.maps.LatLng instead of duck typing.
  • Explicit defineEmits<…>() declaration.

Playground

  • Removed playground/pages/third-parties/google-maps/styled.vue. It relied on the deprecated styles map option which silently conflicts with mapId, and isn't a useful demo on the v1 component.

Out of scope (deferred to follow-up PRs)

These will be split off as separate PRs from #689 with proper deprecations, docs and tests:

  1. Deprecate top-level center/zoom props in favour of mapOptions.
  2. Rename googleMaps expose → mapsApi (with deprecation shim).
  3. OverlayView controlled/uncontrolled API + position type widening.
  4. OverlayView class extraction + reactive position rendering.

What this preserves

No behavioural or API changes, including:

  • center / zoom props still work as top-level props
  • googleMaps expose key unchanged
  • ready event payload shape unchanged
  • Location-query auto-resolution on center still works
  • setCenter skip-if-equal optimisation (from 6fdd533 / 0e4ae9e) preserved
  • Overlay defineModel('open') and overlay expose unchanged

Splits the safe, additive changes out of #689 so they can land
independently of the larger breaking refactor.

- Export `ScriptGoogleMapsProps`/`Emits`/`Slots`/`Expose` and the
  matching `ScriptGoogleMapsOverlayView*` interfaces so consumers can
  type their template refs and event handlers.
- Modernise internal vars: `useTemplateRef` for `rootEl`/`mapEl`,
  `mapsApi` switched to `shallowRef`, internal `ready` ref renamed
  to `isMapReady` for clearer intent.
- Tidy `resolveQueryToLatLng` so the cache always returns a
  consistent `LatLngLiteral` shape.
- Collapse the color-mode logic into a single computed.
- Use `instanceof google.maps.LatLng` to normalise marker positions
  in the overlay component.
- Drop the `styled.vue` playground page; it relied on the deprecated
  `styles` map option which conflicts with `mapId`.

No public API changes — `center`/`zoom` props, `googleMaps` expose,
`ready` event payload, location-query auto-resolution, and the
`setCenter` skip-if-equal optimisation are all preserved.
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment Apr 9, 2026 0:36am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 9, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@692

commit: f209b11

The rule misfires on SFCs that use two <script> blocks (one for
exported types, one for setup): it concatenates the blocks and
flags the setup-block imports as appearing after the exports.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Adds an ESLint config block disabling import/first for **/*.vue. Refactors two Google Maps components: ScriptGoogleMaps.vue (split scripts, exported public TypeScript interfaces, renamed runtime state readyisMapReady, switched some refs to shallowRef/useTemplateRef, changed resolveQueryToLatLng to await/cache results, tightened centering, updated emits/expose types) and ScriptGoogleMapsOverlayView.vue (exported anchor/pane unions, exported props/emits/slots/expose interfaces, typed defineProps/defineEmits/defineSlots/defineExpose, simplified position checks with instanceof). Removes a playground styled Google Maps page and its navigation entry.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: exporting typed interfaces for the google-maps component with internal cleanup refactoring.
Description check ✅ Passed The description is well-related to the changeset, providing clear context about the extracted non-breaking changes, specific component modifications, and deferred work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/google-maps-internal-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
eslint.config.mjs (1)

57-65: Scope the import/first override more narrowly.

This disables the rule for every Vue SFC, including the single-script ones that are not affected by the dual-<script> parsing issue. Prefer targeting just the affected Google Maps components, or using file-local disables there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.mjs` around lines 57 - 65, The current override
blanket-disables the 'import/first' rule for all Vue SFCs (files: ['**/*.vue']);
narrow this to only the affected Google Maps components by changing the files
glob to the specific component paths (e.g. files: ['components/google-maps/**',
'src/components/GoogleMap*.vue'] or the exact filenames) or remove the global
override entirely and add file-local /* eslint-disable import/first */ comments
in the problematic SFCs; update the override entry that references
'import/first' so it targets only those component files instead of '**/*.vue'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue`:
- Around line 223-227: The cache check currently uses "instanceof
google.maps.LatLng" which throws if the Google API isn't loaded; instead, change
the branch that reads from queryToLatLngCache (symbol: queryToLatLngCache and
variable: cached) to detect a LatLng-like object without referencing the global
google (e.g., check if cached.lat is a function to call
cached.lat()/cached.lng(), otherwise treat it as a plain object with numeric
lat/lng properties). Update the conditional so it never accesses google or
google.maps.LatLng and returns { lat: ..., lng: ... } based on the presence/type
of cached.lat.
- Around line 246-255: In resolveQueryToLatLng(), the Promise waits on a
non-immediate watcher of mapsApi which can hang if load() sets mapsApi before
the watcher is created; fix by re-checking mapsApi.value immediately after await
load() and resolving if set, or replace the watcher with watch(mapsApi,
callback, { immediate: true }) (and ensure the watcher is properly torn down) so
the promise always resolves when mapsApi is already populated or changes.

---

Nitpick comments:
In `@eslint.config.mjs`:
- Around line 57-65: The current override blanket-disables the 'import/first'
rule for all Vue SFCs (files: ['**/*.vue']); narrow this to only the affected
Google Maps components by changing the files glob to the specific component
paths (e.g. files: ['components/google-maps/**',
'src/components/GoogleMap*.vue'] or the exact filenames) or remove the global
override entirely and add file-local /* eslint-disable import/first */ comments
in the problematic SFCs; update the override entry that references
'import/first' so it targets only those component files instead of '**/*.vue'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b1bba14-bd88-4e8c-a6b6-da45047c9c34

📥 Commits

Reviewing files that changed from the base of the PR and between 514ed02 and 0fb9b6d.

📒 Files selected for processing (5)
  • eslint.config.mjs
  • packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue
  • packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue
  • playground/pages/index.vue
  • playground/pages/third-parties/google-maps/styled.vue
💤 Files with no reviewable changes (2)
  • playground/pages/index.vue
  • playground/pages/third-parties/google-maps/styled.vue

Comment on lines +246 to +255
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
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file structure and locate the file
fd ScriptGoogleMaps.vue

Repository: 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.vue

Repository: nuxt/scripts

Length of output: 130


🏁 Script executed:

# Get the code context around the problematic lines
cat -n packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | sed -n '230,270p'

Repository: nuxt/scripts

Length of output: 1823


🏁 Script executed:

# Find the load() function definition
rg -A 20 "^\s*(?:async\s+)?load\s*\(" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Repository: nuxt/scripts

Length of output: 38


🏁 Script executed:

# Search for resolveQueryToLatLng to understand its usage
rg -B 5 -A 10 "resolveQueryToLatLng" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Repository: nuxt/scripts

Length of output: 3155


🏁 Script executed:

# Search for mapsApi ref definition and where it gets assigned
rg -B 2 -A 2 "mapsApi\s*=" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | head -40

Repository: nuxt/scripts

Length of output: 144


🏁 Script executed:

# Search for load() function definition
rg -B 5 -A 20 "const load\s*=|function load|async load" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Repository: nuxt/scripts

Length of output: 38


🏁 Script executed:

# Search for where mapsApi.value is assigned
rg "mapsApi\.value\s*=" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Repository: nuxt/scripts

Length of output: 95


🏁 Script executed:

# Get more context around the mapsApi initialization
cat -n packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | sed -n '1,100p'

Repository: nuxt/scripts

Length of output: 3997


🏁 Script executed:

# Search for status variable that's used in proposed fix
rg "status\s*=" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | head -20

Repository: nuxt/scripts

Length of output: 318


🏁 Script executed:

# Search for useScript usage which likely defines load()
rg "useScript|from.*script" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Repository: nuxt/scripts

Length of output: 494


🏁 Script executed:

# Get more context around the mapsApi.value assignment
rg -B 30 "mapsApi\.value\s*=" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Repository: nuxt/scripts

Length of output: 1193


🏁 Script executed:

# Get more lines around the beginning to find setup
cat -n packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | sed -n '100,200p'

Repository: nuxt/scripts

Length of output: 4822


🏁 Script executed:

# Search for status ref definition
rg "const status|status\s*=\s*ref|status\s*=\s*shallowRef" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

Repository: nuxt/scripts

Length of output: 38


🏁 Script executed:

# Get more context around the onLoaded callback
cat -n packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | sed -n '375,395p'

Repository: nuxt/scripts

Length of output: 907


🏁 Script executed:

# Look for useScriptGoogleMaps implementation
fd useScriptGoogleMaps

Repository: nuxt/scripts

Length of output: 38


🏁 Script executed:

# Get the full context of the problematic code and surrounding logic
cat -n packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | sed -n '240,290p'

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 f

Repository: nuxt/scripts

Length of output: 226


🏁 Script executed:

# Check if there's documentation or if load() might complete before watcher registration
cat packages/script/src/runtime/composables/useScriptGoogleMaps.ts | head -100

Repository: nuxt/scripts

Length of output: 150


🏁 Script executed:

# Verify the watch behavior - is it really non-immediate and could miss value changes?
rg "const mapsApi" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue -A 5

Repository: nuxt/scripts

Length of output: 528


🏁 Script executed:

# Check if there are any other similar patterns with immediate watchers in the codebase
rg "watch\(" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | grep -c immediate

Repository: nuxt/scripts

Length of output: 57


🏁 Script executed:

# Find the useScriptGoogleMaps file
fd useScriptGoogleMaps

Repository: nuxt/scripts

Length of output: 38


🏁 Script executed:

# Search for the google-maps registry file
fd "google-maps" --type f | head -20

Repository: nuxt/scripts

Length of output: 656


🏁 Script executed:

# Look for where useScriptGoogleMaps is defined
rg "export.*useScriptGoogleMaps|function useScriptGoogleMaps" packages/

Repository: nuxt/scripts

Length of output: 198


🏁 Script executed:

# Get the full implementation of useScriptGoogleMaps
cat packages/script/src/runtime/registry/google-maps.ts

Repository: nuxt/scripts

Length of output: 2136


🏁 Script executed:

# Let's verify the timing issue by checking if the watcher could miss the value change
# Check the full context where resolveQueryToLatLng is defined and see if status is in scope
cat -n packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | sed -n '190,230p'

Repository: nuxt/scripts

Length of output: 1687


🏁 Script executed:

# Verify if status variable is accessible within the resolveQueryToLatLng function scope
rg -B 50 "async function resolveQueryToLatLng" packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue | grep -E "status|const \{" | head -20

Repository: nuxt/scripts

Length of output: 112


Close the load() / watcher race condition in resolveQueryToLatLng().

If load() completes and triggers onLoaded before the watcher at line 250 is created, mapsApi.value will 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.value after await load(), or use an immediate watcher with error handling via the status variable:

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
}
if (!mapsApi.value) {
await load()
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 })
})
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue`
around lines 246 - 255, In resolveQueryToLatLng(), the Promise waits on a
non-immediate watcher of mapsApi which can hang if load() sets mapsApi before
the watcher is created; fix by re-checking mapsApi.value immediately after await
load() and resolving if set, or replace the watcher with watch(mapsApi,
callback, { immediate: true }) (and ensure the watcher is properly torn down) so
the promise always resolves when mapsApi is already populated or changes.

Copy link
Copy Markdown
Collaborator Author

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 resolveQueryToLatLng is identical to the version this PR replaces (same non-immediate watch(mapsApi, ...) pattern). Out of scope for this internal-cleanup PR; will get its own fix(google-maps): close races in resolveQueryToLatLng follow-up alongside the map.value guard you flagged below.

Copy link
Copy Markdown

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-immediate watch(mapsApi, ...) pattern is identical to what was on main before 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 resolveQueryToLatLng follow-up (covering both the mapsApi watcher race and the map.value guard)?

Reverts the cache-hit normalisation introduced in the previous
commit. The original code stored whatever shape the resolver path
returned (LatLng for Places, LatLngLiteral for proxy) and the
cache-hit path returned the same shape — internally consistent
per path. The normalisation only touched cache hits, which would
have meant the same query returns LatLng on first call but
LatLngLiteral on second call when using the Places fallback.

If we want consistent shapes across both paths, that's a separate
intentional change that should land with a dedicated test.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue (1)

458-465: Minor: loading and error slots can render simultaneously.

When status === 'error' and !isMapReady, both the loading slot (line 460) and error slot (line 464) will render. The loading condition doesn't exclude the error state.

This appears to be existing behavior preserved by the readyisMapReady rename, but worth noting for a future cleanup.

Potential fix
-    <slot v-if="status !== 'awaitingLoad' && !isMapReady" name="loading">
+    <slot v-if="status === 'loading' && !isMapReady" name="loading">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue`
around lines 458 - 465, The loading and error slots can both render when status
=== 'error' and !isMapReady; update the template logic around the loading slot
in ScriptGoogleMaps.vue so the loading slot excludes the error state (e.g.,
change the loading slot's condition to also require status !== 'error' or
convert the slot conditions into an if / else-if chain) so only one of loading
or error renders; target the nodes using isMapReady and status to implement the
fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue`:
- Line 256: The PlacesService is created with new
mapsApi.value!.places.PlacesService(map.value!) which can run before map.value
is set, causing a null reference when resolveQueryToLatLng is called; change to
lazily create or defer creating placesService until the map is initialized
(e.g., move instantiation into resolveQueryToLatLng or check map.value and await
the onLoaded/map-ready promise before calling new
mapsApi.value!.places.PlacesService), and add a defensive guard in
resolveQueryToLatLng to wait for or throw a clear error if map.value is not yet
available; reference symbols: placesService creation line, resolveQueryToLatLng,
mapsApi.value, map.value, onLoaded.

---

Nitpick comments:
In `@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue`:
- Around line 458-465: The loading and error slots can both render when status
=== 'error' and !isMapReady; update the template logic around the loading slot
in ScriptGoogleMaps.vue so the loading slot excludes the error state (e.g.,
change the loading slot's condition to also require status !== 'error' or
convert the slot conditions into an if / else-if chain) so only one of loading
or error renders; target the nodes using isMapReady and status to implement the
fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b85e5cb-e020-4dab-bf88-f33aa5bf310a

📥 Commits

Reviewing files that changed from the base of the PR and between 0fb9b6d and f209b11.

📒 Files selected for processing (1)
  • packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue

return res
}

const placesService = new mapsApi.value!.places.PlacesService(map.value!)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential null reference: map.value may be undefined when PlacesService is instantiated.

resolveQueryToLatLng is exposed publicly and can be called before the map is initialized. After mapsApi.value becomes available, there's a window before map.value is assigned in onLoaded (line 384). Calling this function during that window will throw because PlacesService requires a valid map/element.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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!)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue` at
line 256, The PlacesService is created with new
mapsApi.value!.places.PlacesService(map.value!) which can run before map.value
is set, causing a null reference when resolveQueryToLatLng is called; change to
lazily create or defer creating placesService until the map is initialized
(e.g., move instantiation into resolveQueryToLatLng or check map.value and await
the onLoaded/map-ready promise before calling new
mapsApi.value!.places.PlacesService), and add a defensive guard in
resolveQueryToLatLng to wait for or throw a clear error if map.value is not yet
available; reference symbols: placesService creation line, resolveQueryToLatLng,
mapsApi.value, map.value, onLoaded.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing on main — same new mapsApi.value!.places.PlacesService(map.value!) line existed before this refactor. Real bug, just out of scope for this PR. Tracking it together with the watcher race above for a dedicated fix PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harlan-zw, understood — thanks for the context. The new mapsApi.value!.places.PlacesService(map.value!) null-ref issue predates this PR and is tracked for a dedicated fix PR alongside the watcher race.

Would you like me to open a GitHub issue to track both of these (the map.value null-ref in PlacesService instantiation and the watcher race in resolveQueryToLatLng) so they don't get lost?

@harlan-zw harlan-zw merged commit b59cd35 into main Apr 9, 2026
17 checks passed
@harlan-zw harlan-zw deleted the refactor/google-maps-internal-cleanup branch April 9, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant