From 9537ec28b248d51143ac67b7800b191bb1a5fa86 Mon Sep 17 00:00:00 2001 From: Mark Learst Date: Mon, 29 Dec 2025 19:07:33 -0500 Subject: [PATCH] feat(api): add configurable base URL for fetcher and mutator Add optional baseUrl parameter to FetcherOptions and MutatorOptions interfaces, allowing users to override the default Figma API base URL. Why: - Enables testing against local mocks without patching constants - Supports proxy scenarios for enterprise environments - Allows integration with custom API gateways - Prepares for future self-hosted or alternative endpoints Changes: - src/api/fetcher.ts: Added baseUrl option with default 'https://api.figma.com' - src/api/mutator.ts: Added baseUrl option with default 'https://api.figma.com' - tests/api/fetcher.test.ts: Added 3 tests for baseUrl override behavior - tests/api/mutator.test.ts: Added 3 tests for baseUrl override behavior Usage: const data = await fetcher('/v1/files/abc/variables/local', token, { baseUrl: 'http://localhost:3000' }) Note: When URL is already absolute (starts with http:// or https://), the baseUrl option is ignored. Refs: Codex Audit Item #10 --- src/api/fetcher.ts | 8 +++++- src/api/mutator.ts | 8 +++++- tests/api/fetcher.test.ts | 52 +++++++++++++++++++++++++++++++++++ tests/api/mutator.test.ts | 58 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/api/fetcher.ts b/src/api/fetcher.ts index a6f490e..72fb17c 100644 --- a/src/api/fetcher.ts +++ b/src/api/fetcher.ts @@ -27,6 +27,11 @@ export interface FetcherOptions { * Optional fetch implementation override (useful for testing or custom fetch implementations). */ fetch?: typeof fetch + /** + * Optional base URL override. Defaults to 'https://api.figma.com'. + * Useful for testing with mocks or proxies. + */ + baseUrl?: string } /** @@ -81,6 +86,7 @@ export async function fetcher( signal: providedSignal, timeout, fetch: customFetch = fetch, + baseUrl = FIGMA_API_BASE_URL, } = options ?? {} // Create timeout signal if timeout is provided and no signal is provided @@ -102,7 +108,7 @@ export async function fetcher( const requestUrl = url.startsWith('http://') || url.startsWith('https://') ? url - : `${FIGMA_API_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}` + : `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}` const response = await customFetch(requestUrl, { method: 'GET', diff --git a/src/api/mutator.ts b/src/api/mutator.ts index a81f57e..e0a07e2 100644 --- a/src/api/mutator.ts +++ b/src/api/mutator.ts @@ -24,6 +24,11 @@ export interface MutatorOptions { * Optional fetch implementation override (useful for testing or custom fetch implementations). */ fetch?: typeof fetch + /** + * Optional base URL override. Defaults to 'https://api.figma.com'. + * Useful for testing with mocks or proxies. + */ + baseUrl?: string } /** @@ -81,6 +86,7 @@ export async function mutator( signal: providedSignal, timeout, fetch: customFetch = fetch, + baseUrl = FIGMA_API_BASE_URL, } = options ?? {} // Create timeout signal if timeout is provided and no signal is provided @@ -121,7 +127,7 @@ export async function mutator( const requestUrl = url.startsWith('http://') || url.startsWith('https://') ? url - : `${FIGMA_API_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}` + : `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}` const response = await customFetch(requestUrl, init) diff --git a/tests/api/fetcher.test.ts b/tests/api/fetcher.test.ts index 43b596d..a64074f 100644 --- a/tests/api/fetcher.test.ts +++ b/tests/api/fetcher.test.ts @@ -418,4 +418,56 @@ describe('fetcher', () => { expect(result).toEqual({ data: 'custom' }) }) }) + + describe('baseUrl override', () => { + it('should use custom baseUrl when provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: 'custom' }), + }) + + await fetcher('/v1/files/abc/variables/local', DUMMY_TOKEN, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://proxy.example.com/v1/files/abc/variables/local', + expect.any(Object) + ) + }) + + it('should use default baseUrl when not provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: 'custom' }), + }) + + await fetcher('/v1/files/abc/variables/local', DUMMY_TOKEN, { + fetch: customFetch as typeof fetch, + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://api.figma.com/v1/files/abc/variables/local', + expect.any(Object) + ) + }) + + it('should ignore baseUrl when URL is already absolute', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: 'custom' }), + }) + + await fetcher('https://other.api.com/endpoint', DUMMY_TOKEN, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://other.api.com/endpoint', + expect.any(Object) + ) + }) + }) }) diff --git a/tests/api/mutator.test.ts b/tests/api/mutator.test.ts index 41ceb6f..61e0eaf 100644 --- a/tests/api/mutator.test.ts +++ b/tests/api/mutator.test.ts @@ -485,4 +485,62 @@ describe('mutator', () => { expect(result).toEqual({ id: '123' }) }) }) + + describe('baseUrl override', () => { + it('should use custom baseUrl when provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + body: 'not-null', + json: () => Promise.resolve({ id: '123' }), + }) + + await mutator('/v1/files/abc/variables', token, 'CREATE', body, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://proxy.example.com/v1/files/abc/variables', + expect.any(Object) + ) + }) + + it('should use default baseUrl when not provided', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + body: 'not-null', + json: () => Promise.resolve({ id: '123' }), + }) + + await mutator('/v1/files/abc/variables', token, 'CREATE', body, { + fetch: customFetch as typeof fetch, + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://api.figma.com/v1/files/abc/variables', + expect.any(Object) + ) + }) + + it('should ignore baseUrl when URL is already absolute', async () => { + const customFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + body: 'not-null', + json: () => Promise.resolve({ id: '123' }), + }) + + await mutator('https://other.api.com/endpoint', token, 'CREATE', body, { + fetch: customFetch as typeof fetch, + baseUrl: 'https://proxy.example.com', + }) + + expect(customFetch).toHaveBeenCalledWith( + 'https://other.api.com/endpoint', + expect.any(Object) + ) + }) + }) })