From c64e37745b53727afebc06d1d25aaebba16c461e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:04:04 +0000 Subject: [PATCH 01/16] Initial plan From 0ad614bfc54b93ed80e0e2522480c0bb4d509868 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:15:59 +0000 Subject: [PATCH 02/16] Add comprehensive geolocation functionality with API, caching, and mode support Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- .../dev-utils/src/lib/geo-location.test.ts | 248 ++++++++++++++++++ packages/dev-utils/src/lib/geo-location.ts | 106 ++++++++ packages/dev-utils/src/main.ts | 2 +- 3 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 packages/dev-utils/src/lib/geo-location.test.ts diff --git a/packages/dev-utils/src/lib/geo-location.test.ts b/packages/dev-utils/src/lib/geo-location.test.ts new file mode 100644 index 00000000..e616ec5b --- /dev/null +++ b/packages/dev-utils/src/lib/geo-location.test.ts @@ -0,0 +1,248 @@ +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' + +import { getGeoLocation, mockLocation } from './geo-location.js' + +// Mock fetch +global.fetch = vi.fn() +const mockFetch = vi.mocked(fetch) + +describe('geolocation', () => { + let mockState: { get: vi.Mock; set: vi.Mock } + + beforeEach(() => { + vi.clearAllMocks() + mockState = { + get: vi.fn(), + set: vi.fn(), + } + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('getGeoLocation', () => { + test('returns mock location when mode is "mock"', async () => { + const result = await getGeoLocation({ + mode: 'mock', + state: mockState, + }) + + expect(result).toEqual(mockLocation) + expect(mockState.get).not.toHaveBeenCalled() + expect(mockState.set).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + test('returns custom mock location when geoCountry is provided', async () => { + const result = await getGeoLocation({ + mode: 'cache', + geoCountry: 'FR', + state: mockState, + }) + + expect(result).toEqual({ + city: 'Mock City', + country: { code: 'FR', name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + longitude: 0, + latitude: 0, + timezone: 'UTC', + }) + expect(mockFetch).not.toHaveBeenCalled() + }) + + test('returns cached data when mode is "cache" and data is fresh', async () => { + const cachedData = { + city: 'Cached City', + country: { code: 'CA', name: 'Canada' }, + subdivision: { code: 'ON', name: 'Ontario' }, + longitude: -79.3832, + latitude: 43.6532, + timezone: 'America/Toronto', + } + + mockState.get.mockReturnValue({ + data: cachedData, + timestamp: Date.now() - 1000 * 60 * 60, // 1 hour ago + }) + + const result = await getGeoLocation({ + mode: 'cache', + state: mockState, + }) + + expect(result).toEqual(cachedData) + expect(mockState.get).toHaveBeenCalledWith('geolocation') + expect(mockFetch).not.toHaveBeenCalled() + }) + + test('fetches new data when mode is "cache" but data is stale', async () => { + const staleData = { + city: 'Stale City', + country: { code: 'CA', name: 'Canada' }, + subdivision: { code: 'ON', name: 'Ontario' }, + longitude: -79.3832, + latitude: 43.6532, + timezone: 'America/Toronto', + } + + const freshData = { + city: 'Fresh City', + country: { code: 'US', name: 'United States' }, + subdivision: { code: 'NY', name: 'New York' }, + longitude: -74.006, + latitude: 40.7128, + timezone: 'America/New_York', + } + + mockState.get.mockReturnValue({ + data: staleData, + timestamp: Date.now() - 1000 * 60 * 60 * 25, // 25 hours ago (stale) + }) + + mockFetch.mockResolvedValue({ + json: () => Promise.resolve({ geo: freshData }), + } as Response) + + const result = await getGeoLocation({ + mode: 'cache', + state: mockState, + }) + + expect(result).toEqual(freshData) + expect(mockState.get).toHaveBeenCalledWith('geolocation') + expect(mockState.set).toHaveBeenCalledWith('geolocation', { + data: freshData, + timestamp: expect.any(Number), + }) + expect(mockFetch).toHaveBeenCalledWith('https://netlifind.netlify.app', { + method: 'GET', + signal: expect.any(AbortSignal), + }) + }) + + test('always fetches new data when mode is "update"', async () => { + const cachedData = { + city: 'Cached City', + country: { code: 'CA', name: 'Canada' }, + subdivision: { code: 'ON', name: 'Ontario' }, + longitude: -79.3832, + latitude: 43.6532, + timezone: 'America/Toronto', + } + + const freshData = { + city: 'Fresh City', + country: { code: 'US', name: 'United States' }, + subdivision: { code: 'NY', name: 'New York' }, + longitude: -74.006, + latitude: 40.7128, + timezone: 'America/New_York', + } + + mockState.get.mockReturnValue({ + data: cachedData, + timestamp: Date.now() - 1000 * 60 * 60, // 1 hour ago (fresh) + }) + + mockFetch.mockResolvedValue({ + json: () => Promise.resolve({ geo: freshData }), + } as Response) + + const result = await getGeoLocation({ + mode: 'update', + state: mockState, + }) + + expect(result).toEqual(freshData) + expect(mockState.set).toHaveBeenCalledWith('geolocation', { + data: freshData, + timestamp: expect.any(Number), + }) + expect(mockFetch).toHaveBeenCalledWith('https://netlifind.netlify.app', { + method: 'GET', + signal: expect.any(AbortSignal), + }) + }) + + test('uses cached data when offline is true, even if stale', async () => { + const cachedData = { + city: 'Cached City', + country: { code: 'CA', name: 'Canada' }, + subdivision: { code: 'ON', name: 'Ontario' }, + longitude: -79.3832, + latitude: 43.6532, + timezone: 'America/Toronto', + } + + mockState.get.mockReturnValue({ + data: cachedData, + timestamp: Date.now() - 1000 * 60 * 60 * 25, // 25 hours ago (stale) + }) + + const result = await getGeoLocation({ + mode: 'cache', + offline: true, + state: mockState, + }) + + expect(result).toEqual(cachedData) + expect(mockFetch).not.toHaveBeenCalled() + }) + + test('returns mock location when offline is true and no cached data', async () => { + mockState.get.mockReturnValue(undefined) + + const result = await getGeoLocation({ + mode: 'update', + offline: true, + state: mockState, + }) + + expect(result).toEqual(mockLocation) + expect(mockFetch).not.toHaveBeenCalled() + }) + + test('returns mock location when API request fails', async () => { + mockState.get.mockReturnValue(undefined) + mockFetch.mockRejectedValue(new Error('Network error')) + + const result = await getGeoLocation({ + mode: 'update', + state: mockState, + }) + + expect(result).toEqual(mockLocation) + expect(mockFetch).toHaveBeenCalledWith('https://netlifind.netlify.app', { + method: 'GET', + signal: expect.any(AbortSignal), + }) + }) + + test('uses cached data when country matches geoCountry', async () => { + const cachedData = { + city: 'Paris', + country: { code: 'FR', name: 'France' }, + subdivision: { code: 'IDF', name: 'Île-de-France' }, + longitude: 2.3522, + latitude: 48.8566, + timezone: 'Europe/Paris', + } + + mockState.get.mockReturnValue({ + data: cachedData, + timestamp: Date.now() - 1000 * 60 * 60 * 25, // 25 hours ago (stale) + }) + + const result = await getGeoLocation({ + mode: 'update', + geoCountry: 'FR', + state: mockState, + }) + + expect(result).toEqual(cachedData) + expect(mockFetch).not.toHaveBeenCalled() + }) + }) +}) \ No newline at end of file diff --git a/packages/dev-utils/src/lib/geo-location.ts b/packages/dev-utils/src/lib/geo-location.ts index e0a2adce..e0ca9802 100644 --- a/packages/dev-utils/src/lib/geo-location.ts +++ b/packages/dev-utils/src/lib/geo-location.ts @@ -10,3 +10,109 @@ export const mockLocation: Geolocation = { latitude: 0, timezone: 'UTC', } + +const API_URL = 'https://netlifind.netlify.app' +const STATE_GEO_PROPERTY = 'geolocation' +// 24 hours +const CACHE_TTL = 8.64e7 + +// 10 seconds +const REQUEST_TIMEOUT = 1e4 + +interface State { + get(key: string): unknown + set(key: string, value: unknown): void +} + +/** + * Returns geolocation data from a remote API, the local cache, or a mock location, depending on the + * specified mode. + */ +export const getGeoLocation = async ({ + geoCountry, + mode, + offline = false, + state, +}: { + mode: 'cache' | 'update' | 'mock' + geoCountry?: string | undefined + offline?: boolean | undefined + state: State +}): Promise => { + // Early return for pure mock mode (no geoCountry, no offline) + if (mode === 'mock' && !geoCountry && !offline) { + return mockLocation + } + + const cacheObject = state.get(STATE_GEO_PROPERTY) as { data: Geolocation; timestamp: number } | undefined + + // If we have cached geolocation data and the `--geo` option is set to + // `cache`, let's try to use it. + // Or, if the country we're trying to mock is the same one as we have in the + // cache, let's use the cache instead of the mock. + if (cacheObject !== undefined && (mode === 'cache' || cacheObject.data.country?.code === geoCountry)) { + const age = Date.now() - cacheObject.timestamp + + // Let's use the cached data if it's not older than the TTL. Also, if the + // `--offline` option was used, it's best to use the cached location than + // the mock one. + // Additionally, if we're trying to mock a country that matches the cached country, + // prefer the cached data over the mock. + if (age < CACHE_TTL || offline || cacheObject.data.country?.code === geoCountry) { + return cacheObject.data + } + } + + // If `--country` was used, we also set `--mode=mock`. + if (geoCountry) { + mode = 'mock' + } + + // If the `--geo` option is set to `mock`, we use the default mock location. + // If the `--offline` option was used, we can't talk to the API, so let's + // also use the mock location. Otherwise, use the country code passed in by + // the user. + if (mode === 'mock' || offline || geoCountry) { + if (geoCountry) { + return { + city: 'Mock City', + country: { code: geoCountry, name: 'Mock Country' }, + subdivision: { code: 'SD', name: 'Mock Subdivision' }, + longitude: 0, + latitude: 0, + timezone: 'UTC', + } + } + return mockLocation + } + + // Trying to retrieve geolocation data from the API and caching it locally. + try { + const data = await getGeoLocationFromAPI() + const newCacheObject = { + data, + timestamp: Date.now(), + } + + state.set(STATE_GEO_PROPERTY, newCacheObject) + + return data + } catch { + // We couldn't get geolocation data from the API, so let's return the + // mock location. + return mockLocation + } +} + +/** + * Returns geolocation data from a remote API. + */ +const getGeoLocationFromAPI = async (): Promise => { + const res = await fetch(API_URL, { + method: 'GET', + signal: AbortSignal.timeout(REQUEST_TIMEOUT), + }) + const { geo } = await res.json() as { geo: Geolocation } + + return geo +} diff --git a/packages/dev-utils/src/main.ts b/packages/dev-utils/src/main.ts index b839e7bc..41d0537f 100644 --- a/packages/dev-utils/src/main.ts +++ b/packages/dev-utils/src/main.ts @@ -2,7 +2,7 @@ export { getAPIToken } from './lib/api-token.js' export { shouldBase64Encode } from './lib/base64.js' export { renderFunctionErrorPage } from './lib/errors.js' export { DevEvent, DevEventHandler } from './lib/event.js' -export { type Geolocation, mockLocation } from './lib/geo-location.js' +export { type Geolocation, mockLocation, getGeoLocation } from './lib/geo-location.js' export { ensureNetlifyIgnore } from './lib/gitignore.js' export { headers, toMultiValueHeaders } from './lib/headers.js' export * as globalConfig from './lib/global-config.js' From 8d222265ec5832b835206cd985b4deff9e619b30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:22:06 +0000 Subject: [PATCH 03/16] Address code review feedback: use LocalState class, remove CLI references, hook up to edge functions Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- packages/dev-utils/src/lib/geo-location.ts | 18 ++++++++---------- packages/dev/src/main.ts | 9 +++++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/dev-utils/src/lib/geo-location.ts b/packages/dev-utils/src/lib/geo-location.ts index e0ca9802..4920b912 100644 --- a/packages/dev-utils/src/lib/geo-location.ts +++ b/packages/dev-utils/src/lib/geo-location.ts @@ -1,5 +1,7 @@ import type { Context } from '@netlify/types' +import { LocalState } from './local-state.js' + export type Geolocation = Context['geo'] export const mockLocation: Geolocation = { @@ -19,11 +21,6 @@ const CACHE_TTL = 8.64e7 // 10 seconds const REQUEST_TIMEOUT = 1e4 -interface State { - get(key: string): unknown - set(key: string, value: unknown): void -} - /** * Returns geolocation data from a remote API, the local cache, or a mock location, depending on the * specified mode. @@ -37,7 +34,7 @@ export const getGeoLocation = async ({ mode: 'cache' | 'update' | 'mock' geoCountry?: string | undefined offline?: boolean | undefined - state: State + state: LocalState }): Promise => { // Early return for pure mock mode (no geoCountry, no offline) if (mode === 'mock' && !geoCountry && !offline) { @@ -63,14 +60,15 @@ export const getGeoLocation = async ({ } } - // If `--country` was used, we also set `--mode=mock`. + // If a country code was provided, we use mock mode to generate + // location data for that country. if (geoCountry) { mode = 'mock' } - // If the `--geo` option is set to `mock`, we use the default mock location. - // If the `--offline` option was used, we can't talk to the API, so let's - // also use the mock location. Otherwise, use the country code passed in by + // If the mode is set to `mock`, we use the default mock location. + // If the `offline` option was used, we can't talk to the API, so let's + // also use the mock location. Otherwise, use the country code passed in by // the user. if (mode === 'mock' || offline || geoCountry) { if (geoCountry) { diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index 12b63ff9..0ac7742d 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -4,7 +4,7 @@ import path from 'node:path' import process from 'node:process' import { resolveConfig } from '@netlify/config' -import { ensureNetlifyIgnore, getAPIToken, mockLocation, LocalState, type Logger, HTTPServer } from '@netlify/dev-utils' +import { ensureNetlifyIgnore, getAPIToken, getGeoLocation, mockLocation, LocalState, type Logger, HTTPServer } from '@netlify/dev-utils' import { EdgeFunctionsHandler } from '@netlify/edge-functions/dev' import { FunctionsHandler } from '@netlify/functions/dev' import { HeadersHandler, type HeadersCollector } from '@netlify/headers' @@ -475,11 +475,16 @@ export class NetlifyDev { ), } + const geolocation = await getGeoLocation({ + mode: 'cache', + state, + }) + const edgeFunctionsHandler = new EdgeFunctionsHandler({ configDeclarations: this.#config?.config.edge_functions ?? [], directories: [this.#config?.config.build.edge_functions].filter(Boolean) as string[], env: edgeFunctionsEnv, - geolocation: mockLocation, + geolocation, logger: this.#logger, siteID, siteName: config?.siteInfo.name, From 3538b1ba8228d9cb5b8cda73dfb31a0b5f25c2f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 12:26:51 +0000 Subject: [PATCH 04/16] Address code review feedback: fix import extension, use proper MockFetch pattern, fix type errors Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- .../dev-utils/src/lib/geo-location.test.ts | 60 +++++++++---------- packages/dev-utils/src/lib/geo-location.ts | 2 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/dev-utils/src/lib/geo-location.test.ts b/packages/dev-utils/src/lib/geo-location.test.ts index e616ec5b..55fba188 100644 --- a/packages/dev-utils/src/lib/geo-location.test.ts +++ b/packages/dev-utils/src/lib/geo-location.test.ts @@ -1,13 +1,11 @@ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' import { getGeoLocation, mockLocation } from './geo-location.js' - -// Mock fetch -global.fetch = vi.fn() -const mockFetch = vi.mocked(fetch) +import { MockFetch } from '../test/fetch.js' describe('geolocation', () => { let mockState: { get: vi.Mock; set: vi.Mock } + let mockFetch: MockFetch beforeEach(() => { vi.clearAllMocks() @@ -15,10 +13,11 @@ describe('geolocation', () => { get: vi.fn(), set: vi.fn(), } + mockFetch = new MockFetch() }) afterEach(() => { - vi.restoreAllMocks() + mockFetch.restore() }) describe('getGeoLocation', () => { @@ -31,7 +30,7 @@ describe('geolocation', () => { expect(result).toEqual(mockLocation) expect(mockState.get).not.toHaveBeenCalled() expect(mockState.set).not.toHaveBeenCalled() - expect(mockFetch).not.toHaveBeenCalled() + expect(mockFetch.fulfilled).toBe(true) }) test('returns custom mock location when geoCountry is provided', async () => { @@ -49,7 +48,7 @@ describe('geolocation', () => { latitude: 0, timezone: 'UTC', }) - expect(mockFetch).not.toHaveBeenCalled() + expect(mockFetch.fulfilled).toBe(true) }) test('returns cached data when mode is "cache" and data is fresh', async () => { @@ -74,7 +73,7 @@ describe('geolocation', () => { expect(result).toEqual(cachedData) expect(mockState.get).toHaveBeenCalledWith('geolocation') - expect(mockFetch).not.toHaveBeenCalled() + expect(mockFetch.fulfilled).toBe(true) }) test('fetches new data when mode is "cache" but data is stale', async () => { @@ -101,9 +100,12 @@ describe('geolocation', () => { timestamp: Date.now() - 1000 * 60 * 60 * 25, // 25 hours ago (stale) }) - mockFetch.mockResolvedValue({ - json: () => Promise.resolve({ geo: freshData }), - } as Response) + mockFetch.get({ + url: 'https://netlifind.netlify.app', + response: new Response(JSON.stringify({ geo: freshData }), { + headers: { 'Content-Type': 'application/json' }, + }), + }).inject() const result = await getGeoLocation({ mode: 'cache', @@ -116,10 +118,7 @@ describe('geolocation', () => { data: freshData, timestamp: expect.any(Number), }) - expect(mockFetch).toHaveBeenCalledWith('https://netlifind.netlify.app', { - method: 'GET', - signal: expect.any(AbortSignal), - }) + expect(mockFetch.fulfilled).toBe(true) }) test('always fetches new data when mode is "update"', async () => { @@ -146,9 +145,12 @@ describe('geolocation', () => { timestamp: Date.now() - 1000 * 60 * 60, // 1 hour ago (fresh) }) - mockFetch.mockResolvedValue({ - json: () => Promise.resolve({ geo: freshData }), - } as Response) + mockFetch.get({ + url: 'https://netlifind.netlify.app', + response: new Response(JSON.stringify({ geo: freshData }), { + headers: { 'Content-Type': 'application/json' }, + }), + }).inject() const result = await getGeoLocation({ mode: 'update', @@ -160,10 +162,7 @@ describe('geolocation', () => { data: freshData, timestamp: expect.any(Number), }) - expect(mockFetch).toHaveBeenCalledWith('https://netlifind.netlify.app', { - method: 'GET', - signal: expect.any(AbortSignal), - }) + expect(mockFetch.fulfilled).toBe(true) }) test('uses cached data when offline is true, even if stale', async () => { @@ -188,7 +187,7 @@ describe('geolocation', () => { }) expect(result).toEqual(cachedData) - expect(mockFetch).not.toHaveBeenCalled() + expect(mockFetch.fulfilled).toBe(true) }) test('returns mock location when offline is true and no cached data', async () => { @@ -201,12 +200,16 @@ describe('geolocation', () => { }) expect(result).toEqual(mockLocation) - expect(mockFetch).not.toHaveBeenCalled() + expect(mockFetch.fulfilled).toBe(true) }) test('returns mock location when API request fails', async () => { mockState.get.mockReturnValue(undefined) - mockFetch.mockRejectedValue(new Error('Network error')) + + mockFetch.get({ + url: 'https://netlifind.netlify.app', + response: new Error('Network error'), + }).inject() const result = await getGeoLocation({ mode: 'update', @@ -214,10 +217,7 @@ describe('geolocation', () => { }) expect(result).toEqual(mockLocation) - expect(mockFetch).toHaveBeenCalledWith('https://netlifind.netlify.app', { - method: 'GET', - signal: expect.any(AbortSignal), - }) + expect(mockFetch.fulfilled).toBe(true) }) test('uses cached data when country matches geoCountry', async () => { @@ -242,7 +242,7 @@ describe('geolocation', () => { }) expect(result).toEqual(cachedData) - expect(mockFetch).not.toHaveBeenCalled() + expect(mockFetch.fulfilled).toBe(true) }) }) }) \ No newline at end of file diff --git a/packages/dev-utils/src/lib/geo-location.ts b/packages/dev-utils/src/lib/geo-location.ts index 4920b912..083ffda6 100644 --- a/packages/dev-utils/src/lib/geo-location.ts +++ b/packages/dev-utils/src/lib/geo-location.ts @@ -1,6 +1,6 @@ import type { Context } from '@netlify/types' -import { LocalState } from './local-state.js' +import type { LocalState } from './local-state.js' export type Geolocation = Context['geo'] From 91390ac61599075413f3f4f71184bfbbbae49fcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:59:07 +0000 Subject: [PATCH 05/16] Fix nondeterministic tests by using mock geolocation in test environment Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- packages/dev/src/main.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index fb7756e2..669969aa 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -486,8 +486,15 @@ export class NetlifyDev { ), } + // Use mock mode in test environment to ensure deterministic tests + const isTest = typeof process !== 'undefined' && ( + process.env.NODE_ENV === 'test' || + process.env.VITEST === 'true' || + process.env.npm_lifecycle_event === 'test' + ) + const geolocation = await getGeoLocation({ - mode: 'cache', + mode: isTest ? 'mock' : 'cache', state, }) From 8cbc3e99bc1c4e850ee567397c4ddd935f4f421a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 18:57:03 +0000 Subject: [PATCH 06/16] Add geolocation.mode option to edge functions config and update tests Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- packages/dev/src/main.test.ts | 80 +++++++++++++++++++++++++++++++++++ packages/dev/src/main.ts | 14 +++--- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/packages/dev/src/main.test.ts b/packages/dev/src/main.test.ts index ab6039a6..e63a873e 100644 --- a/packages/dev/src/main.test.ts +++ b/packages/dev/src/main.test.ts @@ -29,6 +29,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -57,6 +62,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -85,6 +95,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -120,6 +135,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello.txt') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -165,6 +185,11 @@ describe('Handling requests', () => { const directory = await fixture.create() const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -205,6 +230,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/shadowed-path.html') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -241,6 +271,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello.html') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -272,6 +307,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello?param1=value1') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -312,6 +352,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -342,6 +387,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -398,6 +448,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -440,6 +495,11 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello') const dev = new NetlifyDev({ projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) await dev.start() @@ -613,6 +673,11 @@ describe('Handling requests', () => { const dev = new NetlifyDev({ apiToken: 'token', projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + } }) const { serverAddress } = await dev.start() @@ -749,6 +814,11 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + }, }) await dev.start() @@ -864,6 +934,11 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + }, }) const { serverAddress } = await dev.start() @@ -963,6 +1038,11 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, + edgeFunctions: { + geolocation: { + mode: 'mock' + } + }, }) await dev.start() diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index 669969aa..720bafb1 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -35,6 +35,9 @@ export interface Features { */ edgeFunctions?: { enabled?: boolean + geolocation?: { + mode?: 'cache' | 'update' | 'mock' + } } /** @@ -147,6 +150,7 @@ export class NetlifyDev { #apiToken?: string #cleanupJobs: (() => Promise)[] #edgeFunctionsHandler?: EdgeFunctionsHandler + #edgeFunctionsConfig?: NetlifyDevOptions['edgeFunctions'] #functionsHandler?: FunctionsHandler #functionsServePath: string #config?: Config @@ -183,6 +187,7 @@ export class NetlifyDev { this.#apiToken = options.apiToken this.#cleanupJobs = [] + this.#edgeFunctionsConfig = options.edgeFunctions this.#features = { blobs: options.blobs?.enabled !== false, edgeFunctions: options.edgeFunctions?.enabled !== false, @@ -486,15 +491,8 @@ export class NetlifyDev { ), } - // Use mock mode in test environment to ensure deterministic tests - const isTest = typeof process !== 'undefined' && ( - process.env.NODE_ENV === 'test' || - process.env.VITEST === 'true' || - process.env.npm_lifecycle_event === 'test' - ) - const geolocation = await getGeoLocation({ - mode: isTest ? 'mock' : 'cache', + mode: this.#edgeFunctionsConfig?.geolocation?.mode ?? 'cache', state, }) From e523edc397067daa08e207e57e1b7e5c681a7f03 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Tue, 15 Jul 2025 16:26:26 -0400 Subject: [PATCH 07/16] chore: reformat --- .../dev-utils/src/lib/geo-location.test.ts | 42 ++++--- packages/dev-utils/src/lib/geo-location.ts | 2 +- packages/dev/src/main.test.ts | 108 +++++++++--------- packages/dev/src/main.ts | 10 +- 4 files changed, 88 insertions(+), 74 deletions(-) diff --git a/packages/dev-utils/src/lib/geo-location.test.ts b/packages/dev-utils/src/lib/geo-location.test.ts index 55fba188..622d5113 100644 --- a/packages/dev-utils/src/lib/geo-location.test.ts +++ b/packages/dev-utils/src/lib/geo-location.test.ts @@ -100,12 +100,14 @@ describe('geolocation', () => { timestamp: Date.now() - 1000 * 60 * 60 * 25, // 25 hours ago (stale) }) - mockFetch.get({ - url: 'https://netlifind.netlify.app', - response: new Response(JSON.stringify({ geo: freshData }), { - headers: { 'Content-Type': 'application/json' }, - }), - }).inject() + mockFetch + .get({ + url: 'https://netlifind.netlify.app', + response: new Response(JSON.stringify({ geo: freshData }), { + headers: { 'Content-Type': 'application/json' }, + }), + }) + .inject() const result = await getGeoLocation({ mode: 'cache', @@ -145,12 +147,14 @@ describe('geolocation', () => { timestamp: Date.now() - 1000 * 60 * 60, // 1 hour ago (fresh) }) - mockFetch.get({ - url: 'https://netlifind.netlify.app', - response: new Response(JSON.stringify({ geo: freshData }), { - headers: { 'Content-Type': 'application/json' }, - }), - }).inject() + mockFetch + .get({ + url: 'https://netlifind.netlify.app', + response: new Response(JSON.stringify({ geo: freshData }), { + headers: { 'Content-Type': 'application/json' }, + }), + }) + .inject() const result = await getGeoLocation({ mode: 'update', @@ -205,11 +209,13 @@ describe('geolocation', () => { test('returns mock location when API request fails', async () => { mockState.get.mockReturnValue(undefined) - - mockFetch.get({ - url: 'https://netlifind.netlify.app', - response: new Error('Network error'), - }).inject() + + mockFetch + .get({ + url: 'https://netlifind.netlify.app', + response: new Error('Network error'), + }) + .inject() const result = await getGeoLocation({ mode: 'update', @@ -245,4 +251,4 @@ describe('geolocation', () => { expect(mockFetch.fulfilled).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/packages/dev-utils/src/lib/geo-location.ts b/packages/dev-utils/src/lib/geo-location.ts index 083ffda6..f66e805f 100644 --- a/packages/dev-utils/src/lib/geo-location.ts +++ b/packages/dev-utils/src/lib/geo-location.ts @@ -110,7 +110,7 @@ const getGeoLocationFromAPI = async (): Promise => { method: 'GET', signal: AbortSignal.timeout(REQUEST_TIMEOUT), }) - const { geo } = await res.json() as { geo: Geolocation } + const { geo } = (await res.json()) as { geo: Geolocation } return geo } diff --git a/packages/dev/src/main.test.ts b/packages/dev/src/main.test.ts index e63a873e..0869e7b4 100644 --- a/packages/dev/src/main.test.ts +++ b/packages/dev/src/main.test.ts @@ -31,9 +31,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -64,9 +64,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -97,9 +97,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -137,9 +137,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -187,9 +187,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -232,9 +232,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -273,9 +273,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -309,9 +309,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -354,9 +354,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -389,9 +389,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -450,9 +450,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -497,9 +497,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) await dev.start() @@ -675,9 +675,9 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: { geolocation: { - mode: 'mock' - } - } + mode: 'mock', + }, + }, }) const { serverAddress } = await dev.start() @@ -814,11 +814,11 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock' - } - }, + edgeFunctions: { + geolocation: { + mode: 'mock', + }, + }, }) await dev.start() @@ -934,11 +934,11 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock' - } - }, + edgeFunctions: { + geolocation: { + mode: 'mock', + }, + }, }) const { serverAddress } = await dev.start() @@ -1038,11 +1038,11 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock' - } - }, + edgeFunctions: { + geolocation: { + mode: 'mock', + }, + }, }) await dev.start() diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index 720bafb1..9ff71956 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -4,7 +4,15 @@ import path from 'node:path' import process from 'node:process' import { resolveConfig } from '@netlify/config' -import { ensureNetlifyIgnore, getAPIToken, getGeoLocation, mockLocation, LocalState, type Logger, HTTPServer } from '@netlify/dev-utils' +import { + ensureNetlifyIgnore, + getAPIToken, + getGeoLocation, + mockLocation, + LocalState, + type Logger, + HTTPServer, +} from '@netlify/dev-utils' import { EdgeFunctionsHandler } from '@netlify/edge-functions/dev' import { FunctionsHandler } from '@netlify/functions/dev' import { HeadersHandler, type HeadersCollector } from '@netlify/headers' From e2a850b3590da12af4584af747863fc369de548d Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Tue, 15 Jul 2025 17:24:04 -0400 Subject: [PATCH 08/16] refactor: fix wrong test types --- packages/dev-utils/src/lib/geo-location.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/dev-utils/src/lib/geo-location.test.ts b/packages/dev-utils/src/lib/geo-location.test.ts index 622d5113..52652001 100644 --- a/packages/dev-utils/src/lib/geo-location.test.ts +++ b/packages/dev-utils/src/lib/geo-location.test.ts @@ -1,10 +1,14 @@ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest' +import type { MockedFunction } from 'vitest' import { getGeoLocation, mockLocation } from './geo-location.js' import { MockFetch } from '../test/fetch.js' describe('geolocation', () => { - let mockState: { get: vi.Mock; set: vi.Mock } + let mockState: { + get: MockedFunction<(key: string) => unknown> + set: MockedFunction<(key: string, value: unknown) => void> + } let mockFetch: MockFetch beforeEach(() => { From 238f0fc75de2d03cace6cc5d0c1a7e1f6d3a6875 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Tue, 15 Jul 2025 17:24:17 -0400 Subject: [PATCH 09/16] build(eslint): disable annoying rule in test --- eslint.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index 4dcae2f7..03d96499 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -119,6 +119,10 @@ export default tseslint.config( }, ], 'n/no-unsupported-features/node-builtins': 'off', + + // Disable unsafe assignment for test files due to vitest expect matchers returning `any` + // See: https://github.com/vitest-dev/vitest/issues/7015 + '@typescript-eslint/no-unsafe-assignment': 'off', }, }, From 4d6fd53285a7f9cf0109a2516d3f1103f3489e45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:16:16 +0000 Subject: [PATCH 10/16] Move geolocation config to top level to apply to both functions and edge functions Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- packages/dev/src/main.test.ts | 118 +++++++++++++++------------------- packages/dev/src/main.ts | 26 +++++--- 2 files changed, 69 insertions(+), 75 deletions(-) diff --git a/packages/dev/src/main.test.ts b/packages/dev/src/main.test.ts index 0869e7b4..93f4af83 100644 --- a/packages/dev/src/main.test.ts +++ b/packages/dev/src/main.test.ts @@ -29,10 +29,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -62,10 +61,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -95,10 +93,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -135,10 +132,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello.txt') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) await dev.start() @@ -185,10 +181,9 @@ describe('Handling requests', () => { const directory = await fixture.create() const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) await dev.start() @@ -230,10 +225,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/shadowed-path.html') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) await dev.start() @@ -271,10 +265,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello.html') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) await dev.start() @@ -307,10 +300,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello?param1=value1') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -352,10 +344,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -387,10 +378,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/from') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -448,10 +438,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -495,10 +484,9 @@ describe('Handling requests', () => { const req = new Request('https://site.netlify/hello') const dev = new NetlifyDev({ projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -673,10 +661,9 @@ describe('Handling requests', () => { const dev = new NetlifyDev({ apiToken: 'token', projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', }, }) @@ -814,11 +801,10 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', + }, }) await dev.start() @@ -934,11 +920,10 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', + }, }) const { serverAddress } = await dev.start() @@ -1038,11 +1023,10 @@ describe('Handling requests', () => { apiURL: context.apiUrl, apiToken: 'token', projectRoot: directory, - edgeFunctions: { - geolocation: { - mode: 'mock', - }, - }, + edgeFunctions: {}, + geolocation: { + mode: 'mock', + }, }) await dev.start() diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index 9ff71956..6b2022cb 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -8,7 +8,6 @@ import { ensureNetlifyIgnore, getAPIToken, getGeoLocation, - mockLocation, LocalState, type Logger, HTTPServer, @@ -43,9 +42,6 @@ export interface Features { */ edgeFunctions?: { enabled?: boolean - geolocation?: { - mode?: 'cache' | 'update' | 'mock' - } } /** @@ -66,6 +62,15 @@ export interface Features { enabled?: boolean } + /** + * Configuration options for geolocation data used by Functions and Edge Functions. + * + * {@link} https://docs.netlify.com/edge-functions/api/#geolocation + */ + geolocation?: { + mode?: 'cache' | 'update' | 'mock' + } + /** * Configuration options for Netlify response headers. * @@ -158,8 +163,8 @@ export class NetlifyDev { #apiToken?: string #cleanupJobs: (() => Promise)[] #edgeFunctionsHandler?: EdgeFunctionsHandler - #edgeFunctionsConfig?: NetlifyDevOptions['edgeFunctions'] #functionsHandler?: FunctionsHandler + #geolocationConfig?: NetlifyDevOptions['geolocation'] #functionsServePath: string #config?: Config #features: { @@ -195,7 +200,7 @@ export class NetlifyDev { this.#apiToken = options.apiToken this.#cleanupJobs = [] - this.#edgeFunctionsConfig = options.edgeFunctions + this.#geolocationConfig = options.geolocation this.#features = { blobs: options.blobs?.enabled !== false, edgeFunctions: options.edgeFunctions?.enabled !== false, @@ -500,7 +505,7 @@ export class NetlifyDev { } const geolocation = await getGeoLocation({ - mode: this.#edgeFunctionsConfig?.geolocation?.mode ?? 'cache', + mode: this.#geolocationConfig?.mode ?? 'cache', state, }) @@ -523,10 +528,15 @@ export class NetlifyDev { this.#config?.config.functionsDirectory ?? path.join(this.#projectRoot, 'netlify/functions') const userFunctionsPathExists = await isDirectory(userFunctionsPath) + const geolocation = await getGeoLocation({ + mode: this.#geolocationConfig?.mode ?? 'cache', + state, + }) + this.#functionsHandler = new FunctionsHandler({ config: this.#config, destPath: this.#functionsServePath, - geolocation: mockLocation, + geolocation, projectRoot: this.#projectRoot, settings: {}, siteId: this.#siteID, From 0c1a52b623f9ea364e6d099d2bea4bdadf206698 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:57:24 +0000 Subject: [PATCH 11/16] Refactor geolocation API from mode to enabled/cache booleans Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- .../dev-utils/src/lib/geo-location.test.ts | 30 ++++++++------ packages/dev-utils/src/lib/geo-location.ts | 40 +++++++++---------- packages/dev/src/main.test.ts | 32 +++++++-------- packages/dev/src/main.ts | 24 +++++++++-- 4 files changed, 73 insertions(+), 53 deletions(-) diff --git a/packages/dev-utils/src/lib/geo-location.test.ts b/packages/dev-utils/src/lib/geo-location.test.ts index 52652001..982f51bf 100644 --- a/packages/dev-utils/src/lib/geo-location.test.ts +++ b/packages/dev-utils/src/lib/geo-location.test.ts @@ -25,9 +25,9 @@ describe('geolocation', () => { }) describe('getGeoLocation', () => { - test('returns mock location when mode is "mock"', async () => { + test('returns mock location when enabled is false', async () => { const result = await getGeoLocation({ - mode: 'mock', + enabled: false, state: mockState, }) @@ -39,7 +39,8 @@ describe('geolocation', () => { test('returns custom mock location when geoCountry is provided', async () => { const result = await getGeoLocation({ - mode: 'cache', + enabled: true, + cache: true, geoCountry: 'FR', state: mockState, }) @@ -55,7 +56,7 @@ describe('geolocation', () => { expect(mockFetch.fulfilled).toBe(true) }) - test('returns cached data when mode is "cache" and data is fresh', async () => { + test('returns cached data when cache is enabled and data is fresh', async () => { const cachedData = { city: 'Cached City', country: { code: 'CA', name: 'Canada' }, @@ -71,7 +72,8 @@ describe('geolocation', () => { }) const result = await getGeoLocation({ - mode: 'cache', + enabled: true, + cache: true, state: mockState, }) @@ -80,7 +82,7 @@ describe('geolocation', () => { expect(mockFetch.fulfilled).toBe(true) }) - test('fetches new data when mode is "cache" but data is stale', async () => { + test('fetches new data when cache is enabled but data is stale', async () => { const staleData = { city: 'Stale City', country: { code: 'CA', name: 'Canada' }, @@ -114,7 +116,8 @@ describe('geolocation', () => { .inject() const result = await getGeoLocation({ - mode: 'cache', + enabled: true, + cache: true, state: mockState, }) @@ -127,7 +130,7 @@ describe('geolocation', () => { expect(mockFetch.fulfilled).toBe(true) }) - test('always fetches new data when mode is "update"', async () => { + test('always fetches new data when cache is disabled', async () => { const cachedData = { city: 'Cached City', country: { code: 'CA', name: 'Canada' }, @@ -161,7 +164,8 @@ describe('geolocation', () => { .inject() const result = await getGeoLocation({ - mode: 'update', + enabled: true, + cache: false, state: mockState, }) @@ -189,7 +193,7 @@ describe('geolocation', () => { }) const result = await getGeoLocation({ - mode: 'cache', + enabled: true, cache: true, offline: true, state: mockState, }) @@ -202,7 +206,7 @@ describe('geolocation', () => { mockState.get.mockReturnValue(undefined) const result = await getGeoLocation({ - mode: 'update', + enabled: true, cache: false, offline: true, state: mockState, }) @@ -222,7 +226,7 @@ describe('geolocation', () => { .inject() const result = await getGeoLocation({ - mode: 'update', + enabled: true, cache: false, state: mockState, }) @@ -246,7 +250,7 @@ describe('geolocation', () => { }) const result = await getGeoLocation({ - mode: 'update', + enabled: true, cache: false, geoCountry: 'FR', state: mockState, }) diff --git a/packages/dev-utils/src/lib/geo-location.ts b/packages/dev-utils/src/lib/geo-location.ts index f66e805f..715c73ab 100644 --- a/packages/dev-utils/src/lib/geo-location.ts +++ b/packages/dev-utils/src/lib/geo-location.ts @@ -23,35 +23,36 @@ const REQUEST_TIMEOUT = 1e4 /** * Returns geolocation data from a remote API, the local cache, or a mock location, depending on the - * specified mode. + * specified options. */ export const getGeoLocation = async ({ geoCountry, - mode, + enabled = true, + cache = true, offline = false, state, }: { - mode: 'cache' | 'update' | 'mock' + enabled?: boolean + cache?: boolean geoCountry?: string | undefined offline?: boolean | undefined state: LocalState }): Promise => { - // Early return for pure mock mode (no geoCountry, no offline) - if (mode === 'mock' && !geoCountry && !offline) { + // Early return for disabled mode (no geoCountry, no offline) + if (!enabled && !geoCountry && !offline) { return mockLocation } const cacheObject = state.get(STATE_GEO_PROPERTY) as { data: Geolocation; timestamp: number } | undefined - // If we have cached geolocation data and the `--geo` option is set to - // `cache`, let's try to use it. + // If we have cached geolocation data and caching is enabled, let's try to use it. // Or, if the country we're trying to mock is the same one as we have in the // cache, let's use the cache instead of the mock. - if (cacheObject !== undefined && (mode === 'cache' || cacheObject.data.country?.code === geoCountry)) { + if (cacheObject !== undefined && (cache || cacheObject.data.country?.code === geoCountry)) { const age = Date.now() - cacheObject.timestamp // Let's use the cached data if it's not older than the TTL. Also, if the - // `--offline` option was used, it's best to use the cached location than + // `offline` option was used, it's best to use the cached location than // the mock one. // Additionally, if we're trying to mock a country that matches the cached country, // prefer the cached data over the mock. @@ -60,17 +61,8 @@ export const getGeoLocation = async ({ } } - // If a country code was provided, we use mock mode to generate - // location data for that country. - if (geoCountry) { - mode = 'mock' - } - - // If the mode is set to `mock`, we use the default mock location. - // If the `offline` option was used, we can't talk to the API, so let's - // also use the mock location. Otherwise, use the country code passed in by - // the user. - if (mode === 'mock' || offline || geoCountry) { + // If a country code was provided or geolocation is disabled, we use mock location data. + if (geoCountry || !enabled) { if (geoCountry) { return { city: 'Mock City', @@ -84,9 +76,17 @@ export const getGeoLocation = async ({ return mockLocation } + // If the `offline` option was used, we can't talk to the API, so let's + // use the mock location. + if (offline) { + return mockLocation + } + // Trying to retrieve geolocation data from the API and caching it locally. try { const data = await getGeoLocationFromAPI() + + // Always cache the data for future use const newCacheObject = { data, timestamp: Date.now(), diff --git a/packages/dev/src/main.test.ts b/packages/dev/src/main.test.ts index 93f4af83..5d0468ec 100644 --- a/packages/dev/src/main.test.ts +++ b/packages/dev/src/main.test.ts @@ -31,7 +31,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -63,7 +63,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -95,7 +95,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -134,7 +134,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) await dev.start() @@ -183,7 +183,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) await dev.start() @@ -227,7 +227,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) await dev.start() @@ -267,7 +267,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) await dev.start() @@ -302,7 +302,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -346,7 +346,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -380,7 +380,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -440,7 +440,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -486,7 +486,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -663,7 +663,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -803,7 +803,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -922,7 +922,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) @@ -1025,7 +1025,7 @@ describe('Handling requests', () => { projectRoot: directory, edgeFunctions: {}, geolocation: { - mode: 'mock', + enabled: false, }, }) diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index 6b2022cb..6e65130d 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -65,10 +65,24 @@ export interface Features { /** * Configuration options for geolocation data used by Functions and Edge Functions. * - * {@link} https://docs.netlify.com/edge-functions/api/#geolocation + * {@link} https://docs.netlify.com/build/edge-functions/api/#geo */ geolocation?: { - mode?: 'cache' | 'update' | 'mock' + /** + * Use real geolocation data. Involves making an API call. When disabled, a + * mock location is used. + * + * {@default} true + */ + enabled?: boolean + + /** + * Cache the result of the API call. When disabled, the location is retrieved + * each time. + * + * {@default} true + */ + cache?: boolean } /** @@ -505,7 +519,8 @@ export class NetlifyDev { } const geolocation = await getGeoLocation({ - mode: this.#geolocationConfig?.mode ?? 'cache', + enabled: this.#geolocationConfig?.enabled ?? true, + cache: this.#geolocationConfig?.cache ?? true, state, }) @@ -529,7 +544,8 @@ export class NetlifyDev { const userFunctionsPathExists = await isDirectory(userFunctionsPath) const geolocation = await getGeoLocation({ - mode: this.#geolocationConfig?.mode ?? 'cache', + enabled: this.#geolocationConfig?.enabled ?? true, + cache: this.#geolocationConfig?.cache ?? true, state, }) From 34e158bd06d233fb8d2080480de1f768d20a8968 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:00:32 +0000 Subject: [PATCH 12/16] Refactor geolocation config to follow existing pattern and simplify API Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- .../dev-utils/src/lib/geo-location.test.ts | 44 ++----------------- packages/dev-utils/src/lib/geo-location.ts | 20 +++------ packages/dev/src/main.test.ts | 18 ++++---- packages/dev/src/main.ts | 12 ++--- 4 files changed, 22 insertions(+), 72 deletions(-) diff --git a/packages/dev-utils/src/lib/geo-location.test.ts b/packages/dev-utils/src/lib/geo-location.test.ts index 982f51bf..d443c80e 100644 --- a/packages/dev-utils/src/lib/geo-location.test.ts +++ b/packages/dev-utils/src/lib/geo-location.test.ts @@ -177,44 +177,6 @@ describe('geolocation', () => { expect(mockFetch.fulfilled).toBe(true) }) - test('uses cached data when offline is true, even if stale', async () => { - const cachedData = { - city: 'Cached City', - country: { code: 'CA', name: 'Canada' }, - subdivision: { code: 'ON', name: 'Ontario' }, - longitude: -79.3832, - latitude: 43.6532, - timezone: 'America/Toronto', - } - - mockState.get.mockReturnValue({ - data: cachedData, - timestamp: Date.now() - 1000 * 60 * 60 * 25, // 25 hours ago (stale) - }) - - const result = await getGeoLocation({ - enabled: true, cache: true, - offline: true, - state: mockState, - }) - - expect(result).toEqual(cachedData) - expect(mockFetch.fulfilled).toBe(true) - }) - - test('returns mock location when offline is true and no cached data', async () => { - mockState.get.mockReturnValue(undefined) - - const result = await getGeoLocation({ - enabled: true, cache: false, - offline: true, - state: mockState, - }) - - expect(result).toEqual(mockLocation) - expect(mockFetch.fulfilled).toBe(true) - }) - test('returns mock location when API request fails', async () => { mockState.get.mockReturnValue(undefined) @@ -226,7 +188,8 @@ describe('geolocation', () => { .inject() const result = await getGeoLocation({ - enabled: true, cache: false, + enabled: true, + cache: false, state: mockState, }) @@ -250,7 +213,8 @@ describe('geolocation', () => { }) const result = await getGeoLocation({ - enabled: true, cache: false, + enabled: true, + cache: false, geoCountry: 'FR', state: mockState, }) diff --git a/packages/dev-utils/src/lib/geo-location.ts b/packages/dev-utils/src/lib/geo-location.ts index 715c73ab..0252758e 100644 --- a/packages/dev-utils/src/lib/geo-location.ts +++ b/packages/dev-utils/src/lib/geo-location.ts @@ -29,17 +29,15 @@ export const getGeoLocation = async ({ geoCountry, enabled = true, cache = true, - offline = false, state, }: { enabled?: boolean cache?: boolean geoCountry?: string | undefined - offline?: boolean | undefined state: LocalState }): Promise => { - // Early return for disabled mode (no geoCountry, no offline) - if (!enabled && !geoCountry && !offline) { + // Early return for disabled mode (no geoCountry) + if (!enabled && !geoCountry) { return mockLocation } @@ -51,12 +49,10 @@ export const getGeoLocation = async ({ if (cacheObject !== undefined && (cache || cacheObject.data.country?.code === geoCountry)) { const age = Date.now() - cacheObject.timestamp - // Let's use the cached data if it's not older than the TTL. Also, if the - // `offline` option was used, it's best to use the cached location than - // the mock one. + // Let's use the cached data if it's not older than the TTL. // Additionally, if we're trying to mock a country that matches the cached country, // prefer the cached data over the mock. - if (age < CACHE_TTL || offline || cacheObject.data.country?.code === geoCountry) { + if (age < CACHE_TTL || cacheObject.data.country?.code === geoCountry) { return cacheObject.data } } @@ -76,16 +72,10 @@ export const getGeoLocation = async ({ return mockLocation } - // If the `offline` option was used, we can't talk to the API, so let's - // use the mock location. - if (offline) { - return mockLocation - } - // Trying to retrieve geolocation data from the API and caching it locally. try { const data = await getGeoLocationFromAPI() - + // Always cache the data for future use const newCacheObject = { data, diff --git a/packages/dev/src/main.test.ts b/packages/dev/src/main.test.ts index 5d0468ec..a87adcf9 100644 --- a/packages/dev/src/main.test.ts +++ b/packages/dev/src/main.test.ts @@ -802,9 +802,9 @@ describe('Handling requests', () => { apiToken: 'token', projectRoot: directory, edgeFunctions: {}, - geolocation: { - enabled: false, - }, + geolocation: { + enabled: false, + }, }) await dev.start() @@ -921,9 +921,9 @@ describe('Handling requests', () => { apiToken: 'token', projectRoot: directory, edgeFunctions: {}, - geolocation: { - enabled: false, - }, + geolocation: { + enabled: false, + }, }) const { serverAddress } = await dev.start() @@ -1024,9 +1024,9 @@ describe('Handling requests', () => { apiToken: 'token', projectRoot: directory, edgeFunctions: {}, - geolocation: { - enabled: false, - }, + geolocation: { + enabled: false, + }, }) await dev.start() diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index 6e65130d..8a9c80d6 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -68,12 +68,6 @@ export interface Features { * {@link} https://docs.netlify.com/build/edge-functions/api/#geo */ geolocation?: { - /** - * Use real geolocation data. Involves making an API call. When disabled, a - * mock location is used. - * - * {@default} true - */ enabled?: boolean /** @@ -186,6 +180,7 @@ export class NetlifyDev { edgeFunctions: boolean environmentVariables: boolean functions: boolean + geolocation: boolean headers: boolean images: boolean redirects: boolean @@ -220,6 +215,7 @@ export class NetlifyDev { edgeFunctions: options.edgeFunctions?.enabled !== false, environmentVariables: options.environmentVariables?.enabled !== false, functions: options.functions?.enabled !== false, + geolocation: options.geolocation?.enabled !== false, headers: options.headers?.enabled !== false, images: options.images?.enabled !== false, redirects: options.redirects?.enabled !== false, @@ -519,7 +515,7 @@ export class NetlifyDev { } const geolocation = await getGeoLocation({ - enabled: this.#geolocationConfig?.enabled ?? true, + enabled: this.#features.geolocation, cache: this.#geolocationConfig?.cache ?? true, state, }) @@ -544,7 +540,7 @@ export class NetlifyDev { const userFunctionsPathExists = await isDirectory(userFunctionsPath) const geolocation = await getGeoLocation({ - enabled: this.#geolocationConfig?.enabled ?? true, + enabled: this.#features.geolocation, cache: this.#geolocationConfig?.cache ?? true, state, }) From c19f4812493082a75071211eb562a49c692df434 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:37:23 +0000 Subject: [PATCH 13/16] Fix vite plugin integration tests for middleware features message Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- packages/vite-plugin/src/main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite-plugin/src/main.test.ts b/packages/vite-plugin/src/main.test.ts index 601c5a0e..1fc481a3 100644 --- a/packages/vite-plugin/src/main.test.ts +++ b/packages/vite-plugin/src/main.test.ts @@ -208,7 +208,7 @@ defined on your team and site and much more. Run npx netlify init to get started expect(mockLogger.info).toHaveBeenNthCalledWith(1, 'Environment loaded', expect.objectContaining({})) expect(mockLogger.info).toHaveBeenNthCalledWith( 2, - 'Middleware loaded. Emulating features: blobs, environmentVariables, functions, headers, images, redirects, static.', + 'Middleware loaded. Emulating features: blobs, environmentVariables, functions, geolocation, headers, images, redirects, static.', expect.objectContaining({}), ) expect(mockLogger.info).toHaveBeenNthCalledWith( From 45614027f3ffd6e2f8ba39dd22b2705fa895c610 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:10:12 +0000 Subject: [PATCH 14/16] refactor: share geolocation instance across functions and edge functions Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- packages/dev/src/main.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index 8a9c80d6..a89652d2 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -491,6 +491,8 @@ export class NetlifyDev { }) } + let geolocation: Awaited> | undefined + if (this.#features.edgeFunctions) { const edgeFunctionsEnv = { // User-defined env vars + documented runtime env vars @@ -514,7 +516,7 @@ export class NetlifyDev { ), } - const geolocation = await getGeoLocation({ + geolocation ??= await getGeoLocation({ enabled: this.#features.geolocation, cache: this.#geolocationConfig?.cache ?? true, state, @@ -539,7 +541,7 @@ export class NetlifyDev { this.#config?.config.functionsDirectory ?? path.join(this.#projectRoot, 'netlify/functions') const userFunctionsPathExists = await isDirectory(userFunctionsPath) - const geolocation = await getGeoLocation({ + geolocation ??= await getGeoLocation({ enabled: this.#features.geolocation, cache: this.#geolocationConfig?.cache ?? true, state, From 3fc487df0fa1ba0ffc8c811d70238df643320500 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:33:09 +0000 Subject: [PATCH 15/16] Remove geoCountry parameter and simplify geolocation API Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- .../dev-utils/src/lib/geo-location.test.ts | 45 ------------------- packages/dev-utils/src/lib/geo-location.ts | 29 ++---------- 2 files changed, 4 insertions(+), 70 deletions(-) diff --git a/packages/dev-utils/src/lib/geo-location.test.ts b/packages/dev-utils/src/lib/geo-location.test.ts index d443c80e..e768c8bf 100644 --- a/packages/dev-utils/src/lib/geo-location.test.ts +++ b/packages/dev-utils/src/lib/geo-location.test.ts @@ -37,25 +37,6 @@ describe('geolocation', () => { expect(mockFetch.fulfilled).toBe(true) }) - test('returns custom mock location when geoCountry is provided', async () => { - const result = await getGeoLocation({ - enabled: true, - cache: true, - geoCountry: 'FR', - state: mockState, - }) - - expect(result).toEqual({ - city: 'Mock City', - country: { code: 'FR', name: 'Mock Country' }, - subdivision: { code: 'SD', name: 'Mock Subdivision' }, - longitude: 0, - latitude: 0, - timezone: 'UTC', - }) - expect(mockFetch.fulfilled).toBe(true) - }) - test('returns cached data when cache is enabled and data is fresh', async () => { const cachedData = { city: 'Cached City', @@ -196,31 +177,5 @@ describe('geolocation', () => { expect(result).toEqual(mockLocation) expect(mockFetch.fulfilled).toBe(true) }) - - test('uses cached data when country matches geoCountry', async () => { - const cachedData = { - city: 'Paris', - country: { code: 'FR', name: 'France' }, - subdivision: { code: 'IDF', name: 'Île-de-France' }, - longitude: 2.3522, - latitude: 48.8566, - timezone: 'Europe/Paris', - } - - mockState.get.mockReturnValue({ - data: cachedData, - timestamp: Date.now() - 1000 * 60 * 60 * 25, // 25 hours ago (stale) - }) - - const result = await getGeoLocation({ - enabled: true, - cache: false, - geoCountry: 'FR', - state: mockState, - }) - - expect(result).toEqual(cachedData) - expect(mockFetch.fulfilled).toBe(true) - }) }) }) diff --git a/packages/dev-utils/src/lib/geo-location.ts b/packages/dev-utils/src/lib/geo-location.ts index 0252758e..dd6f5033 100644 --- a/packages/dev-utils/src/lib/geo-location.ts +++ b/packages/dev-utils/src/lib/geo-location.ts @@ -26,52 +26,31 @@ const REQUEST_TIMEOUT = 1e4 * specified options. */ export const getGeoLocation = async ({ - geoCountry, enabled = true, cache = true, state, }: { enabled?: boolean cache?: boolean - geoCountry?: string | undefined state: LocalState }): Promise => { - // Early return for disabled mode (no geoCountry) - if (!enabled && !geoCountry) { + // Early return for disabled mode + if (!enabled) { return mockLocation } const cacheObject = state.get(STATE_GEO_PROPERTY) as { data: Geolocation; timestamp: number } | undefined // If we have cached geolocation data and caching is enabled, let's try to use it. - // Or, if the country we're trying to mock is the same one as we have in the - // cache, let's use the cache instead of the mock. - if (cacheObject !== undefined && (cache || cacheObject.data.country?.code === geoCountry)) { + if (cacheObject !== undefined && cache) { const age = Date.now() - cacheObject.timestamp // Let's use the cached data if it's not older than the TTL. - // Additionally, if we're trying to mock a country that matches the cached country, - // prefer the cached data over the mock. - if (age < CACHE_TTL || cacheObject.data.country?.code === geoCountry) { + if (age < CACHE_TTL) { return cacheObject.data } } - // If a country code was provided or geolocation is disabled, we use mock location data. - if (geoCountry || !enabled) { - if (geoCountry) { - return { - city: 'Mock City', - country: { code: geoCountry, name: 'Mock Country' }, - subdivision: { code: 'SD', name: 'Mock Subdivision' }, - longitude: 0, - latitude: 0, - timezone: 'UTC', - } - } - return mockLocation - } - // Trying to retrieve geolocation data from the API and caching it locally. try { const data = await getGeoLocationFromAPI() From 23710018c111a6b7ae1f8cb1e03f141b52e33a2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:55:14 +0000 Subject: [PATCH 16/16] feat: use Geolocation type for geolocation variable Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --- packages/dev/src/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/dev/src/main.ts b/packages/dev/src/main.ts index a89652d2..64d95c75 100644 --- a/packages/dev/src/main.ts +++ b/packages/dev/src/main.ts @@ -8,6 +8,7 @@ import { ensureNetlifyIgnore, getAPIToken, getGeoLocation, + type Geolocation, LocalState, type Logger, HTTPServer, @@ -491,7 +492,7 @@ export class NetlifyDev { }) } - let geolocation: Awaited> | undefined + let geolocation: Geolocation | undefined if (this.#features.edgeFunctions) { const edgeFunctionsEnv = {