Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,184 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

## 4.0.0 (2025-12-29)

### ⚠️ BREAKING CHANGES

#### **`useFigmaToken` Export Changed**

`useFigmaToken` is now a **named export** instead of a default export for consistency with the rest of the library:

```tsx
// Before (3.x) - NO LONGER WORKS
import useFigmaToken from '@figma-vars/hooks'

// After (4.0) - USE THIS
import { useFigmaToken } from '@figma-vars/hooks'
```
Comment on lines +17 to +23
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The breaking change documentation claims useFigmaToken is now a named export, but this hook is not exported from the main package entry point (src/index.ts). Users trying to import it as shown will get an error. Either add the export to src/index.ts, or update the documentation to clarify this hook is not part of the public API.

Copilot uses AI. Check for mistakes.

**Why?** Named exports enable better tree-shaking, align with other hooks in the library, and support future package splitting.

### ✨ Added - New Utilities

#### **`withRetry()` - Automatic Retry with Exponential Backoff**

New utility for wrapping async operations with automatic retry logic, especially useful for rate-limited API calls:

```ts
import { withRetry } from '@figma-vars/hooks'

const fetchWithRetry = withRetry(() => fetcher('/api/endpoint', token), {
maxRetries: 3,
initialDelayMs: 1000,
backoffMultiplier: 2,
maxDelayMs: 30000,
retryOnlyRateLimits: true, // Only retry 429 errors
onRetry: (attempt, delayMs, error) => {
console.log(`Retry ${attempt} after ${delayMs}ms`)
},
})

const data = await fetchWithRetry()
```

**Features:**

- Respects `Retry-After` header from Figma API
- Configurable exponential backoff
- Optional callback for retry notifications
- Can retry all errors or only rate limits (429)

#### **`redactToken()` - Safe Token Logging**

New utility to safely redact Figma tokens for logging or display:

```ts
import { redactToken } from '@figma-vars/hooks'

redactToken('figd_abc123xyz789secret')
// Returns: 'figd_***...***cret'
Comment on lines +64 to +65
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The example output is incorrect. According to the actual implementation in src/utils/redactToken.ts, redactToken('figd_abc123xyz789secret') would return 'figd_***...ret' (showing 5 chars at start and 3 at end by default), not 'figd_...***cret' (4 chars at end). The default visibleEnd is 3, not 4.

Copilot uses AI. Check for mistakes.

redactToken('short', { prefixLength: 2, suffixLength: 2 })
// Returns: '***...***' (too short, fully redacted)
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The example is misleading. The token 'short' (5 characters) with prefixLength: 2, suffixLength: 2 would require 5 characters minimum (2+2+1), so it would be fully masked as '' (5 asterisks), not '...*'. The actual implementation masks short tokens with asterisks matching the token length, not using the redaction string.

Suggested change
// Returns: '***...***' (too short, fully redacted)
// Returns: '*****' (too short, fully redacted)

Copilot uses AI. Check for mistakes.

redactToken(null) // Returns: ''
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The return value for null is incorrect. According to the actual implementation in src/utils/redactToken.ts, redactToken(null) returns '[no token]', not an empty string ''. The default emptyPlaceholder is '[no token]'.

Suggested change
redactToken(null) // Returns: ''
redactToken(null) // Returns: '[no token]'

Copilot uses AI. Check for mistakes.
```

**Options:**

- `prefixLength` - Characters to show at start (default: 5)
- `suffixLength` - Characters to show at end (default: 5)
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The default value for suffixLength is incorrect. According to the actual implementation in src/utils/redactToken.ts, the default visibleEnd is 3, not 5. The documentation should say "Characters to show at end (default: 3)".

Suggested change
- `suffixLength` - Characters to show at end (default: 5)
- `suffixLength` - Characters to show at end (default: 3)

Copilot uses AI. Check for mistakes.
- `redactionString` - Replacement string (default: `'***...***'`)
Comment on lines +73 to +77
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The parameter names are incorrect. According to the actual implementation in src/utils/redactToken.ts, the options are named visibleStart and visibleEnd, not prefixLength and suffixLength. Also, there is no redactionString option in the actual implementation.

Copilot uses AI. Check for mistakes.

#### **`baseUrl` Option for API Utilities**

Both `fetcher` and `mutator` now accept a `baseUrl` option to override the default Figma API endpoint:

```ts
import { fetcher, mutator } from '@figma-vars/hooks/core'

// Use a mock server for testing
const data = await fetcher('/v1/files/KEY/variables/local', token, {
baseUrl: 'http://localhost:3000',
})

// Or use Figma Enterprise endpoint
await mutator('/v1/files/KEY/variables', token, 'UPDATE', payload, {
baseUrl: 'https://enterprise.figma.com',
})
```

#### **`caseInsensitive` Option for `filterVariables()`**

Added case-insensitive name matching to the `filterVariables` utility:

```ts
import { filterVariables } from '@figma-vars/hooks'

// Case-sensitive (default)
filterVariables(variables, { name: 'Primary' })
// Matches: "Primary Color", not "primary color"

// Case-insensitive
filterVariables(variables, { name: 'primary', caseInsensitive: true })
// Matches: "Primary Color", "primary color", "PRIMARY"
```

### 🐛 Fixed - Critical Bug Fixes

#### **SWR Key Caching Issue**

Fixed a critical bug where fallback data could be cached under live API keys when both credentials and fallback were provided. Now:

- If `fallbackFile` is provided, data is always cached under fallback-specific keys
- Prevents stale fallback data from blocking actual API calls
- Ensures cache consistency when switching between fallback and live modes

#### **Improved Error Parsing for Non-JSON Responses**

Fixed error handling when Figma API returns HTML or plain text errors (e.g., 502 Bad Gateway):

```ts
// Before: Generic "An API error occurred" message
// After: Actual response body (truncated to 200 chars for HTML)
```

The `fetcher` and `mutator` now check `Content-Type` headers and parse errors appropriately.

### 📚 Documentation

#### **Mutation Return Type Semantics**

Added comprehensive JSDoc documentation explaining mutation hook return values:

- `mutate()` returns `Promise<TData | undefined>`
- On success: returns `TData`
- On error with `throwOnError: false` (default): returns `undefined`, error available in state
- On error with `throwOnError: true`: throws the error

All mutation hooks (`useCreateVariable`, `useUpdateVariable`, `useDeleteVariable`, `useBulkUpdateVariables`) now have detailed examples showing all three usage patterns.

### 🔧 Changed

- **Named Exports**: `useFigmaToken` changed from default to named export (see Breaking Changes)
- **Coverage Comments**: Changed `/* istanbul ignore next */` to `/* c8 ignore next */` for proper Vitest V8 coverage exclusion

### 🎯 Migration Guide (3.x → 4.0)

**1. Update `useFigmaToken` import (required if used):**

```tsx
// Find this in your code:
import useFigmaToken from '@figma-vars/hooks'

// Replace with:
import { useFigmaToken } from '@figma-vars/hooks'
```

**2. New utilities are opt-in:**

- Use `withRetry()` to add automatic retry logic to API calls
- Use `redactToken()` before logging tokens
- Use `baseUrl` option when testing with mock servers
- Use `caseInsensitive: true` for flexible variable filtering

**3. Automatic improvements (no action needed):**

- SWR caching now works correctly with fallback + live credentials
- Better error messages for non-JSON API responses
- Improved documentation for mutation return types

### 🙏 Acknowledgments

This release addresses issues identified through a comprehensive Codex audit. All 25 audit items have been validated and resolved where applicable.

## 3.1.1 (2025-12-28)

### 📚 Documentation

- Minor documentation file updates

## 3.1.0 (2025-12-27)

### 🐛 Fixed - Critical TypeScript & Runtime Issues
Expand Down
105 changes: 82 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,25 @@ A fast, typed React 19.2.3 hooks library for the Figma Variables API: fetch, upd

Built for the modern web, this library provides a suite of hooks to fetch, manage, and mutate your design tokens/variables, making it easy to sync them between Figma and your React applications, Storybooks, or design system dashboards.

![Status](https://img.shields.io/badge/status-stable-brightgreen)
![CI](https://github.com/marklearst/figma-vars-hooks/actions/workflows/ci.yml/badge.svg)
[![codecov](https://codecov.io/gh/marklearst/figma-vars-hooks/branch/main/graph/badge.svg)](https://codecov.io/gh/marklearst/figma-vars-hooks)
![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
![License](https://img.shields.io/github/license/marklearst/figma-vars-hooks)
![GitHub last commit](https://img.shields.io/github/last-commit/marklearst/figma-vars-hooks)
![GitHub code size](https://img.shields.io/github/languages/code-size/marklearst/figma-vars-hooks)

## 📌 Why 3.1.1

- ✨ **New DX Features**: SWR configuration support, error handling utilities, cache invalidation helpers
- 🔧 **React 19.2 Ready**: Optimized hooks with proper cleanup and stable function references
- 🛡️ **Better Error Handling**: `FigmaApiError` class with HTTP status codes for better error differentiation
- ✅ **Type Safety**: Removed unsafe type assertions, improved type definitions throughout
- 🚀 **Performance**: Hardened SWR usage (stable keys, `null` to disable, cleaner fallback handling)
| Package | Quality | Activity |
| --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [![npm version](https://img.shields.io/npm/v/%40figma-vars%2Fhooks)](https://www.npmjs.com/package/@figma-vars/hooks) | ![CI](https://github.com/marklearst/figma-vars-hooks/actions/workflows/ci.yml/badge.svg) | ![GitHub last commit](https://img.shields.io/github/last-commit/marklearst/figma-vars-hooks) |
| ![npm downloads](https://img.shields.io/npm/dm/%40figma-vars%2Fhooks) | [![codecov](https://codecov.io/gh/marklearst/figma-vars-hooks/branch/main/graph/badge.svg)](https://codecov.io/gh/marklearst/figma-vars-hooks) | ![Status](https://img.shields.io/badge/status-stable-brightgreen) |
| ![bundle size](https://img.shields.io/bundlephobia/minzip/%40figma-vars%2Fhooks) | ![Test Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen) | ![License](https://img.shields.io/github/license/marklearst/figma-vars-hooks) |
| ![node version](https://img.shields.io/node/v/%40figma-vars%2Fhooks) | ![TypeScript](https://img.shields.io/badge/TypeScript-100%25_Strict-blue?logo=typescript) | |

## 📌 Why 4.0.0

- ✨ **New Utilities**: `withRetry()` for automatic retry with exponential backoff, `redactToken()` for safe logging
- 🔧 **Flexible API**: `baseUrl` option for fetcher/mutator, `caseInsensitive` option for filterVariables
- 🛡️ **Better Error Handling**: Improved parsing for non-JSON API responses (HTML, plain text)
- 🐛 **Critical Bug Fix**: SWR cache keys now correctly separate fallback and live data
- 📚 **Improved Docs**: Comprehensive mutation return type documentation with examples
- 📦 **Modern Tooling**: Node 20+ toolchain, strict TypeScript, and ESM-first packaging with CJS interop
- 🖥️ **CLI Export Tool**: Automate variable exports with `figma-vars-export` for CI/CD (Enterprise required)

> ⚠️ **Breaking Change**: `useFigmaToken` is now a named export. See [Migration Guide](#-migration-guide-3x--40).

## 🚀 Features at a Glance

- **Modern React 19.2 hooks** for variables, collections, modes, and published variables
Expand Down Expand Up @@ -424,11 +425,13 @@ Customize SWR behavior globally through the provider:

### Utilities

- **Filtering**: `filterVariables` (filter by type, name, etc.)
- **Error Handling**: `isFigmaApiError`, `getErrorStatus`, `getErrorMessage`, `hasErrorStatus`
- **Filtering**: `filterVariables` (filter by type, name, with optional `caseInsensitive` matching)
- **Retry**: `withRetry` (automatic retry with exponential backoff for rate limits)
- **Security**: `redactToken` (safely redact tokens for logging/display)
Comment on lines +429 to +430
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The new utilities withRetry and redactToken are documented as part of 4.0.0 but are not exported from the main package entry point. Users will get import errors when trying to use them as shown in the examples. These utilities should be added to the exports in src/index.ts lines 86-92.

Suggested change
- **Retry**: `withRetry` (automatic retry with exponential backoff for rate limits)
- **Security**: `redactToken` (safely redact tokens for logging/display)

Copilot uses AI. Check for mistakes.
- **Error Handling**: `isFigmaApiError`, `getErrorStatus`, `getErrorMessage`, `hasErrorStatus`, `isRateLimited`, `getRetryAfter`
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The new error handling utilities isRateLimited and getRetryAfter are documented in the API cheat sheet but are not exported from the main package entry point (src/index.ts). These should be added to the exports at lines 86-92 for users to access them.

Suggested change
- **Error Handling**: `isFigmaApiError`, `getErrorStatus`, `getErrorMessage`, `hasErrorStatus`, `isRateLimited`, `getRetryAfter`
- **Error Handling**: `isFigmaApiError`, `getErrorStatus`, `getErrorMessage`, `hasErrorStatus`

Copilot uses AI. Check for mistakes.
- **Type Guards**: `isLocalVariablesResponse`, `isPublishedVariablesResponse`, `validateFallbackData` (runtime validation)
- **SWR Keys**: `getVariablesKey`, `getPublishedVariablesKey`, `getInvalidationKeys` (centralized cache key construction)
- **Core helpers**: `fetcher`, `mutator`, constants for endpoints and headers
- **Core helpers**: `fetcher`, `mutator` (with `baseUrl` option), constants for endpoints and headers

### Types

Expand All @@ -454,13 +457,30 @@ Customize SWR behavior globally through the provider:
- Never commit PATs or file keys to git, Storybook static builds, or client bundles.
- Use environment variables (`process.env` / `import.meta.env`) and secret managers; keep them server-side where possible.
- Prefer `fallbackFile` with `token={null}`/`fileKey={null}` for demos and public Storybooks.
- Avoid logging tokens or keys; scrub them from error messages and analytics.
- Use `redactToken()` when logging tokens for debugging:

```ts
import { redactToken } from '@figma-vars/hooks'

// Safe logging
console.log('Using token:', redactToken(token))
// Output: "Using token: figd_***...***cret"
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The example output is incorrect. According to the actual implementation in src/utils/redactToken.ts, redactToken would return 'figd_***...ret' (showing 5 chars at start and 3 at end by default), not 'figd_...***cret' (4 chars at end). The default visibleEnd is 3, not 4.

Suggested change
// Output: "Using token: figd_***...***cret"
// Output: "Using token: figd_***...***ret"

Copilot uses AI. Check for mistakes.
```

## 📈 Rate Limits

- Figma enforces per-token limits. Rely on SWR/TanStack caching, avoid unnecessary refetches, and prefer fallback JSON for static sites.
- Use `swrConfig` to customize `dedupingInterval` and `errorRetryCount` to optimize API usage.
- Handle `429` rate limit errors with `isFigmaApiError` and implement exponential backoff if needed.
- Use `withRetry()` utility for automatic retry with exponential backoff on 429 errors:

```ts
import { withRetry, fetcher } from '@figma-vars/hooks'
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The import import { withRetry, fetcher } from '@figma-vars/hooks' will fail. withRetry is not exported from the main package entry point (src/index.ts), and fetcher is only available from '@figma-vars/hooks/core'. The correct import should be import { fetcher } from '@figma-vars/hooks/core' and import { withRetry } from '@figma-vars/hooks/utils' (or add these to main exports).

Suggested change
import { withRetry, fetcher } from '@figma-vars/hooks'
import { fetcher } from '@figma-vars/hooks/core'
import { withRetry } from '@figma-vars/hooks/utils'

Copilot uses AI. Check for mistakes.

const fetchWithRetry = withRetry(() => fetcher('/v1/files/KEY/variables/local', token), {
maxRetries: 3,
onRetry: (attempt, delay) => console.log(`Retry ${attempt}...`),
})
```

## 📚 Storybook & Next.js

Expand Down Expand Up @@ -511,11 +531,50 @@ export function Providers({ children }: { children: React.ReactNode }) {
- `pnpm run build`, `pnpm test`, `pnpm run test:coverage`
- `pnpm run check:publint`, `pnpm run check:attw`, `pnpm run check:size`

## 🧭 Release Checklist (for 3.1.0)
## 🧭 Release Checklist (for 4.0.0)

- Run `pnpm run check:release`
- Tag `v3.1.0` (CI publishes to npm)
- Update dist-tags on npm if needed (`latest` → 3.1.0)
- Run `pnpm version major` (creates `v4.0.0` tag)
- CI publishes to npm automatically
- Update dist-tags on npm if needed (`latest` → 4.0.0)

## 🔄 Migration Guide (3.x → 4.0)

### Breaking Change: `useFigmaToken` Export

```tsx
// Before (3.x) - NO LONGER WORKS
import useFigmaToken from '@figma-vars/hooks'

// After (4.0) - USE THIS
import { useFigmaToken } from '@figma-vars/hooks'
```
Comment on lines +545 to +551
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The migration guide assumes useFigmaToken is exported from the main package, but it's not present in src/index.ts. Users following this guide will get an import error. Either add useFigmaToken to the exports in src/index.ts line 57-67, or clarify in the documentation that this hook is not part of the public API.

Copilot uses AI. Check for mistakes.

### New Utilities (opt-in)

```ts
import { withRetry, redactToken, filterVariables } from '@figma-vars/hooks'
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The import statement import { withRetry, redactToken, filterVariables } from '@figma-vars/hooks' will fail because withRetry and redactToken are not exported from the main package entry point (src/index.ts). Users need to either import from '@figma-vars/hooks/utils' or these utilities need to be added to the main exports.

Suggested change
import { withRetry, redactToken, filterVariables } from '@figma-vars/hooks'
import { filterVariables } from '@figma-vars/hooks'
import { withRetry, redactToken } from '@figma-vars/hooks/utils'

Copilot uses AI. Check for mistakes.

// Automatic retry with exponential backoff
const fetchWithRetry = withRetry(() => myApiCall(), { maxRetries: 3 })

// Safe token logging
console.log('Token:', redactToken(token)) // "figd_***...***cret"
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

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

The example output is incorrect. According to the actual implementation in src/utils/redactToken.ts, redactToken would return 'figd_***...ret' (showing 5 chars at start and 3 at end by default), not 'figd_...***cret' (4 chars at end). The default visibleEnd is 3, not 4.

Suggested change
console.log('Token:', redactToken(token)) // "figd_***...***cret"
console.log('Token:', redactToken(token)) // "figd_***...***ret"

Copilot uses AI. Check for mistakes.

// Case-insensitive filtering
filterVariables(vars, { name: 'primary', caseInsensitive: true })
```

### Custom API Base URL

```ts
import { fetcher, mutator } from '@figma-vars/hooks/core'

// Use mock server for testing
await fetcher('/v1/files/KEY/variables/local', token, {
baseUrl: 'http://localhost:3000',
})
```

---

Expand Down