figma-console-mcp 1.33.2 β 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
- package/dist/cloudflare/core/tokens/alias-resolver.js +15 -2
- package/dist/cloudflare/core/tokens/config.js +10 -0
- package/dist/cloudflare/core/tokens/dialect.js +232 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +37 -3
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +35 -14
- package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
- package/dist/cloudflare/core/tokens/index.js +2 -1
- package/dist/cloudflare/core/tokens/schemas.js +4 -0
- package/dist/cloudflare/core/tokens-tools.js +942 -81
- package/dist/cloudflare/core/websocket-connector.js +42 -0
- package/dist/cloudflare/core/write-tools.js +308 -49
- package/dist/cloudflare/index.js +7 -7
- package/dist/core/figma-connector.d.ts +12 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/tokens/alias-resolver.d.ts +16 -0
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
- package/dist/core/tokens/alias-resolver.js +15 -2
- package/dist/core/tokens/alias-resolver.js.map +1 -1
- package/dist/core/tokens/config.d.ts +28 -0
- package/dist/core/tokens/config.d.ts.map +1 -1
- package/dist/core/tokens/config.js +10 -0
- package/dist/core/tokens/config.js.map +1 -1
- package/dist/core/tokens/dialect.d.ts +107 -0
- package/dist/core/tokens/dialect.d.ts.map +1 -0
- package/dist/core/tokens/dialect.js +233 -0
- package/dist/core/tokens/dialect.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +2 -0
- package/dist/core/tokens/figma-converter.d.ts.map +1 -1
- package/dist/core/tokens/figma-converter.js +37 -3
- package/dist/core/tokens/figma-converter.js.map +1 -1
- package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
- package/dist/core/tokens/formatters/dtcg.js +35 -14
- package/dist/core/tokens/formatters/dtcg.js.map +1 -1
- package/dist/core/tokens/formatters/json.d.ts.map +1 -1
- package/dist/core/tokens/formatters/json.js +28 -10
- package/dist/core/tokens/formatters/json.js.map +1 -1
- package/dist/core/tokens/index.d.ts +2 -1
- package/dist/core/tokens/index.d.ts.map +1 -1
- package/dist/core/tokens/index.js +2 -1
- package/dist/core/tokens/index.js.map +1 -1
- package/dist/core/tokens/schemas.d.ts +3 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -1
- package/dist/core/tokens/schemas.js +4 -0
- package/dist/core/tokens/schemas.js.map +1 -1
- package/dist/core/tokens/types.d.ts +29 -0
- package/dist/core/tokens/types.d.ts.map +1 -1
- package/dist/core/tokens/types.js.map +1 -1
- package/dist/core/tokens-tools.d.ts +211 -12
- package/dist/core/tokens-tools.d.ts.map +1 -1
- package/dist/core/tokens-tools.js +942 -81
- package/dist/core/tokens-tools.js.map +1 -1
- package/dist/core/websocket-connector.d.ts +38 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +42 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +308 -49
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +5 -4
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +288 -1
- package/figma-desktop-bridge/ui.html +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
> **Your design system as an API.** Model Context Protocol server that bridges design and developmentβgiving AI assistants complete access to Figma for **extraction**, **creation**, **debugging**, and **bidirectional token sync**.
|
|
10
10
|
|
|
11
|
-
> **π
|
|
11
|
+
> **π Bidirectional Token Sync v2 + DTCG 2025.10 (v1.34.0):** `figma_import_tokens` now applies the *whole* diff plan β creates, renames, alias re-targeting, and replace-gated deletes β for a true codeβFigma round-trip; exports speak DTCG 2025.10 on request (opt-in, legacy default byte-identical); scopes/codeSyntax round-trip; `figma_setup_design_tokens` accepts alias values; and the new `figma_create_component_set` builds a full variant set from an axes matrix in one call. **Plugin re-import required** (`code.js` + `ui.html` changed). [See what's new β](CHANGELOG.md#1340---2026-07-03)
|
|
12
12
|
|
|
13
13
|
## What is this?
|
|
14
14
|
|
|
15
15
|
Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
16
16
|
|
|
17
17
|
- **π¨ Design system extraction** - Pull variables, components, and styles
|
|
18
|
-
- **π Bidirectional token sync** - Export Figma variables to DTCG JSON +
|
|
18
|
+
- **π Bidirectional token sync** - Export Figma variables to DTCG JSON (legacy or 2025.10 dialect) + 9 more formats; push code-side edits back to Figma with full apply β creates, renames, alias re-targeting, and replace-gated deletes. Replaces Style Dictionary and Tokens Studio's export pipeline.
|
|
19
19
|
- **πΈ Visual debugging** - Take screenshots for context
|
|
20
|
-
- **βοΈ Design creation** - Create UI components, frames, and layouts directly in Figma
|
|
20
|
+
- **βοΈ Design creation** - Create UI components, frames, and layouts directly in Figma β including one-call component-set creation from a variant axes matrix
|
|
21
21
|
- **π§ Variable management** - Create, update, rename, and delete design tokens
|
|
22
22
|
- **π° Version history & time-series awareness** - List versions, diff snapshots, generate markdown changelogs, trace property/variant introduction via binary-search blame
|
|
23
23
|
- **β‘ Real-time monitoring** - Watch console logs from the Desktop Bridge plugin
|
|
@@ -55,9 +55,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
55
55
|
| Real-time monitoring (console, selection) | β
| β | β |
|
|
56
56
|
| Desktop Bridge plugin | β
| β
| β |
|
|
57
57
|
| Requires Node.js | Yes | **No** | No |
|
|
58
|
-
| **Total tools available** | **
|
|
58
|
+
| **Total tools available** | **107** | **96** | **9** |
|
|
59
59
|
|
|
60
|
-
> **Bottom line:** Remote SSE is **read-only** with
|
|
60
|
+
> **Bottom line:** Remote SSE is **read-only** with 9 tools. **Cloud Mode** unlocks write access (96 tools) from web AI clients without Node.js. NPX/Local Git gives the full 107 tools with real-time monitoring.
|
|
61
61
|
|
|
62
62
|
---
|
|
63
63
|
|
|
@@ -65,7 +65,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
65
65
|
|
|
66
66
|
**Best for:** Designers who want full AI-assisted design capabilities.
|
|
67
67
|
|
|
68
|
-
**What you get:** All
|
|
68
|
+
**What you get:** All 107 tools including design creation, variable management, and component instantiation.
|
|
69
69
|
|
|
70
70
|
#### Prerequisites
|
|
71
71
|
|
|
@@ -162,7 +162,7 @@ Create a simple frame with a blue background
|
|
|
162
162
|
|
|
163
163
|
**Best for:** Developers who want to modify source code or contribute to the project.
|
|
164
164
|
|
|
165
|
-
**What you get:** Same
|
|
165
|
+
**What you get:** Same 107 tools as NPX, plus full source code access.
|
|
166
166
|
|
|
167
167
|
#### Quick Setup
|
|
168
168
|
|
|
@@ -251,7 +251,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
|
|
|
251
251
|
|
|
252
252
|
**Best for:** Using Claude.ai, v0, Replit, or Lovable to create and modify Figma designs β no Node.js required.
|
|
253
253
|
|
|
254
|
-
**What you get:**
|
|
254
|
+
**What you get:** 96 tools including full write access β design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
|
|
255
255
|
|
|
256
256
|
#### Prerequisites
|
|
257
257
|
|
|
@@ -308,7 +308,7 @@ AI Client β Cloud MCP Server β Durable Object Relay β Desktop Bridge Plugi
|
|
|
308
308
|
| Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
|
|
309
309
|
|---------|-------------------|------------|-----------|------------|
|
|
310
310
|
| **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
|
|
311
|
-
| **Total tools** | **
|
|
311
|
+
| **Total tools** | **107** | **96** | **107** | **9** (read-only) |
|
|
312
312
|
| **Design creation** | β
| β
| β
| β |
|
|
313
313
|
| **Variable management** | β
| β
| β
| β |
|
|
314
314
|
| **Component instantiation** | β
| β
| β
| β |
|
|
@@ -323,7 +323,7 @@ AI Client β Cloud MCP Server β Durable Object Relay β Desktop Bridge Plugi
|
|
|
323
323
|
| **Automatic updates** | β
(`@latest`) | β
| Manual (`git pull`) | β
|
|
|
324
324
|
| **Source code access** | β | β | β
| β |
|
|
325
325
|
|
|
326
|
-
> **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full
|
|
326
|
+
> **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 107 tools.
|
|
327
327
|
|
|
328
328
|
**π [Complete Feature Comparison](docs/mode-comparison.md)**
|
|
329
329
|
|
|
@@ -415,6 +415,10 @@ When you first use design system tools:
|
|
|
415
415
|
- Create frames, shapes, text, components
|
|
416
416
|
- Apply auto-layout, styles, effects
|
|
417
417
|
- Build complete UI mockups programmatically
|
|
418
|
+
- `figma_create_component_set` - **Create a component set with variants in one declarative call**
|
|
419
|
+
- Generate every variant combination from an axes matrix (e.g. `{ State: ["default", "hover", "disabled"], Size: ["sm", "lg"] }` β 6 variants) off a base component, or combine existing components
|
|
420
|
+
- `Prop=Value` variant naming, `combineAsVariants` under the hood, optional auto-arranged labeled grid
|
|
421
|
+
- Returns each variant's key, ready for `figma_instantiate_component`
|
|
418
422
|
- `figma_arrange_component_set` - **Organize variants into professional component sets**
|
|
419
423
|
- Convert multiple component variants into a proper Figma component set
|
|
420
424
|
- Applies native purple dashed border visualization automatically
|
|
@@ -432,8 +436,8 @@ When you first use design system tools:
|
|
|
432
436
|
- `figma_generate_component_doc` - Generate platform-agnostic markdown documentation by merging Figma design data with code-side info
|
|
433
437
|
|
|
434
438
|
### π Token Sync (Local Mode + Cloud Mode)
|
|
435
|
-
- `figma_export_tokens` - **Export Figma variables to design token files in your codebase.** Canonical DTCG JSON
|
|
436
|
-
- `figma_import_tokens` - **Push code-side token edits back to Figma.**
|
|
439
|
+
- `figma_export_tokens` - **Export Figma variables to design token files in your codebase.** Canonical DTCG JSON (legacy hex dialect by default, or DTCG 2025.10 object colors/dimensions via `dtcgDialect: "2025"`) plus CSS, Tailwind v4/v3, SCSS, TS, JSON, Style Dictionary, and Tokens Studio formats. Diff-aware merge against existing source files (only writes what changed). `tokens.config.json` autodiscovery means zero-arg calls after first setup. Scopes and codeSyntax metadata round-trip via `$extensions`. Replaces Style Dictionary and Tokens Studio's export pipeline for popular styling methods.
|
|
440
|
+
- `figma_import_tokens` - **Push code-side token edits back to Figma with a full apply phase.** Diffs against current Figma state, then applies value updates, **creates** missing collections/variables, applies **renames**, writes real **alias** (`VARIABLE_ALIAS`) references, and β only under `strategy: "replace"` β deletes Figma-only variables. Round-trip safe β Figma variable IDs preserved in DTCG `$extensions["figma-console-mcp"]` so renames on either side don't create duplicates. Accepts both DTCG dialects. Dry-run strategy for safe previews. In Cloud Mode, pass tokens inline via `payload` or `files` (no local filesystem access).
|
|
437
441
|
|
|
438
442
|
### π§ Variable Management (Local Mode + Cloud Mode)
|
|
439
443
|
- `figma_create_variable_collection` - Create new variable collections with modes
|
|
@@ -446,7 +450,7 @@ When you first use design system tools:
|
|
|
446
450
|
- `figma_rename_mode` - Rename existing modes
|
|
447
451
|
- `figma_batch_create_variables` - Create up to 100 variables in one call (10-50x faster)
|
|
448
452
|
- `figma_batch_update_variables` - Update up to 100 variable values in one call
|
|
449
|
-
- `figma_setup_design_tokens` - Create complete token system (collection + modes + variables) atomically
|
|
453
|
+
- `figma_setup_design_tokens` - Create complete token system (collection + modes + variables) atomically β values accept DTCG brace references (`"{color.blue.600}"`) that resolve to real variable aliases
|
|
450
454
|
|
|
451
455
|
### π FigJam Board Tools (Local Mode + Cloud Mode)
|
|
452
456
|
- `figjam_create_sticky` - Create a sticky note with color options
|
|
@@ -669,7 +673,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
|
|
|
669
673
|
- The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
|
|
670
674
|
- The server tries port 9223 first, then automatically falls back through ports 9224β9232 if needed
|
|
671
675
|
- The plugin scans all ports in the range and connects to every active server it finds
|
|
672
|
-
- All
|
|
676
|
+
- All 107 tools work through the WebSocket transport
|
|
673
677
|
|
|
674
678
|
**Multiple files:** The WebSocket server supports multiple simultaneous plugin connections β one per open Figma file. Each connection is tracked by file key with independent state (selection, document changes, console logs).
|
|
675
679
|
|
|
@@ -808,9 +812,10 @@ The architecture supports adding new apps with minimal boilerplate β each app
|
|
|
808
812
|
|
|
809
813
|
## π€οΈ Roadmap
|
|
810
814
|
|
|
811
|
-
**Current Status:** v1.
|
|
815
|
+
**Current Status:** v1.34.0 (Stable) - Production-ready. Latest: Bidirectional Token Sync v2 + DTCG 2025.10 β `figma_import_tokens` now applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and the new `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5Γ is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog β fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figmaβcode token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **107 tools** (Local) / **96 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
|
|
812
816
|
|
|
813
817
|
**Recent Releases:**
|
|
818
|
+
- [x] **v1.34.0** - Bidirectional Token Sync v2 + DTCG 2025.10. `figma_import_tokens` now applies the *complete* diff plan: missing collections and variables are created (with modes, inferred/recorded types, and values set in dependency order β aliases in a second pass), token-path renames route to the update phase by round-trip variable ID (no more create+delete pairs that would permanently destroy the original under `replace`), reference values write real `{ type: "VARIABLE_ALIAS", id }` payloads via a four-tier resolver, and deletes are strictly gated behind `strategy: "replace"`. `figma_export_tokens` gains `dtcgDialect: "2025"` (object-form colors from full-precision floats, object dimensions) while the legacy default stays byte-identical; import accepts both dialects unconditionally with dialect-insensitive diff normalization. Variable `scopes` + `codeSyntax` round-trip through `$extensions["figma-console-mcp"]`. `figma_setup_design_tokens` accepts DTCG brace references (`"{color.blue.600}"`) that resolve to real aliases, including forward references. New tool `figma_create_component_set` builds a variant set from an axes matrix (or combines existing components) with `Prop=Value` naming, optional auto-arranged grid, and variant keys in the response β with count-scaled timeouts and rollback on failure. **Plugin re-import required** (`code.js` + `ui.html` changed β the component-set handler and relay). 183 tests across the token/write-tools suites.
|
|
814
819
|
- [x] **v1.33.2** - Version-handshake false-positive fix. The v1.33.0 handshake compared the plugin's reported version against the server's *package* version, so server-only releases (like the v1.33.1 dependency sweep) flagged every up-to-date plugin as stale and pushed the re-import banner for files that hadn't changed. The server now compares against the `PLUGIN_VERSION` embedded in the `figma-desktop-bridge/code.js` it ships β exactly what a re-import would install β and `PLUGIN_VERSION` itself now means "last release in which plugin files changed" (release tooling bumps it only when `figma-desktop-bridge/` actually changed since the last tag). `figma_get_status` gains `transport.websocket.bundledPluginVersion`; `figma_diagnose` blames the right version. No new tools, **no plugin re-import required** (one-time exception: if you re-imported at v1.33.1, the banner appears once more β clear it with one final re-import). 1245 tests passing (9 new).
|
|
815
820
|
- [x] **v1.33.1** - Security dependency sweep. All runtime and critical npm audit alerts resolved via in-range bumps (`ws` 8.21.0, `hono` 4.12.27, `undici` 7.28.0, `handlebars` 4.7.9 β the lone critical, dev-only β plus `lodash`, `path-to-regexp`, `basic-ftp`, `fast-uri`, `vite`). `wrangler` deliberately held at 4.72.0 because newer versions require Node β₯22; the only residual audit findings are inside wrangler/miniflare's dev-time toolchain, which never ships in the npm package or Worker bundle. Supersedes dependabot PRs #81/#82/#84. No code changes, no API changes, no plugin re-import. 1236 tests passing unchanged.
|
|
816
821
|
- [x] **v1.33.0** - Connection UX overhaul + full-codebase audit. The plugin's status pill now derives from live connection state instead of Figma's variables loading (it used to glow green with zero MCP servers connected); HTTP `/health` auto-discovery reconnects restarted servers automatically (including one-dead-among-live, previously a permanent dead end); a version handshake banners the plugin UI when a re-import is needed and surfaces the mismatch in `figma_get_status`/`figma_diagnose`; cloud pairing config survives plugin reopen and its status line is derived + labeled (no more orphaned "Disconnected" under a green pill); all plugin copy is designer-language. The audit fixed 33 verified issues: lossless DTCG multi-mode round-trips, set-qualified cross-collection aliases, TIMING/EASING mapped to DTCG `duration`/`cubicBezier`, two cache-poisoning bugs (the "search returns 0 components" reports), a CSWSH origin bypass (`startsWith` β exact match), post-sleep reaper kill-safety (plus a shell-free `/health` probe with `os.devNull` so Windows curl can't false-negative a healthy sibling), branch-URL correctness across REST tools, and bridge-first `figma_take_screenshot`. `figma_arrange_component_set` now rearranges variants in place so placed instances survive. No new tools; **plugin re-import required** (`code.js` + `ui.html` changed β and the new handshake makes this the last one you have to discover on your own). 1236 tests passing (33 new).
|
|
@@ -839,7 +844,8 @@ The architecture supports adding new apps with minimal boilerplate β each app
|
|
|
839
844
|
- [x] **v1.7.0** - MCP Apps (Token Browser, Design System Dashboard), batch variable operations, design-code parity tools.
|
|
840
845
|
|
|
841
846
|
**Coming Next:**
|
|
842
|
-
- [ ] **Token sync β
|
|
847
|
+
- [ ] **Token sync β non-DTCG input parsers** - Parsers for non-DTCG input (Tokens Studio, CSS vars, Tailwind v4, Tailwind v3 config, SCSS, Style Dictionary v3, JSON flat/nested) so `figma_import_tokens` can ingest the same formats it exports. (The import-side apply expansion β creates, replace-gated deletes, alias-target updates β shipped in v1.34.0.)
|
|
848
|
+
- [ ] **Cross-library variable resolution** - Resolve cross-library aliases via `getVariableByIdAsync` so they render as real `var(--target)` references in exports instead of comments.
|
|
843
849
|
- [ ] **Component template library** - Common UI pattern generation
|
|
844
850
|
- [ ] **Visual regression testing** - Screenshot diff capabilities
|
|
845
851
|
|
|
@@ -180,6 +180,24 @@ export class CloudWebSocketConnector {
|
|
|
180
180
|
}
|
|
181
181
|
return this.sendCommand('INSTANTIATE_COMPONENT', params);
|
|
182
182
|
}
|
|
183
|
+
async createComponentSet(params) {
|
|
184
|
+
// Timeout scales with variant count β the plugin builds all variants in
|
|
185
|
+
// one uncancellable pass, so a fixed 30s ceiling on big matrices reports
|
|
186
|
+
// failure while the set still gets created (retry β duplicates).
|
|
187
|
+
// Mirrors componentSetTimeoutMs in websocket-connector.ts (not imported:
|
|
188
|
+
// that module pulls the Node 'ws' stack into the Workers bundle).
|
|
189
|
+
let variantCount = 1;
|
|
190
|
+
if (params.componentIds?.length) {
|
|
191
|
+
variantCount = params.componentIds.length;
|
|
192
|
+
}
|
|
193
|
+
else if (params.properties) {
|
|
194
|
+
for (const values of Object.values(params.properties)) {
|
|
195
|
+
variantCount *= Math.max(1, values?.length ?? 1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const timeout = Math.min(120000, Math.max(30000, variantCount * 1200)) + 5000;
|
|
199
|
+
return this.sendCommand('CREATE_COMPONENT_SET', params, timeout);
|
|
200
|
+
}
|
|
183
201
|
// ============================================================================
|
|
184
202
|
// Node manipulation
|
|
185
203
|
// ============================================================================
|
|
@@ -37,6 +37,19 @@ export function slugifySetName(name) {
|
|
|
37
37
|
* a `warnings` array get one warning per ambiguous bare path.
|
|
38
38
|
*/
|
|
39
39
|
export function buildTokenIndex(doc, warnings) {
|
|
40
|
+
const index = new Map();
|
|
41
|
+
for (const [key, entry] of buildTokenLookup(doc, warnings)) {
|
|
42
|
+
index.set(key, entry.token);
|
|
43
|
+
}
|
|
44
|
+
return index;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Set-aware variant of buildTokenIndex β identical key scheme and ambiguity
|
|
48
|
+
* rules (set-qualified keys always; bare-path fallback only when unambiguous
|
|
49
|
+
* across sets), but each entry carries the owning set's name alongside the
|
|
50
|
+
* token. buildTokenIndex delegates to this so the two can never drift.
|
|
51
|
+
*/
|
|
52
|
+
export function buildTokenLookup(doc, warnings) {
|
|
40
53
|
const index = new Map();
|
|
41
54
|
// barePath β owning entries, used to detect cross-set ambiguity.
|
|
42
55
|
const bareOwners = new Map();
|
|
@@ -44,7 +57,7 @@ export function buildTokenIndex(doc, warnings) {
|
|
|
44
57
|
const setKey = slugifySetName(set.name);
|
|
45
58
|
for (const token of set.tokens) {
|
|
46
59
|
const bare = token.path.join(".");
|
|
47
|
-
index.set(`${setKey}.${bare}`, token);
|
|
60
|
+
index.set(`${setKey}.${bare}`, { setName: set.name, token });
|
|
48
61
|
const owners = bareOwners.get(bare) ?? [];
|
|
49
62
|
owners.push({ setName: set.name, token });
|
|
50
63
|
bareOwners.set(bare, owners);
|
|
@@ -55,7 +68,7 @@ export function buildTokenIndex(doc, warnings) {
|
|
|
55
68
|
for (const [bare, owners] of bareOwners) {
|
|
56
69
|
if (owners.length === 1) {
|
|
57
70
|
if (!index.has(bare))
|
|
58
|
-
index.set(bare, owners[0]
|
|
71
|
+
index.set(bare, owners[0]);
|
|
59
72
|
}
|
|
60
73
|
else if (warnings) {
|
|
61
74
|
warnings.push(`Token path "${bare}" exists in multiple collections (${owners
|
|
@@ -42,6 +42,16 @@ const OutputTargetSchema = z.object({
|
|
|
42
42
|
* for CSS/SCSS/Tailwind/etc. since they can't natively express aliases).
|
|
43
43
|
*/
|
|
44
44
|
resolveAliases: z.boolean().optional(),
|
|
45
|
+
/**
|
|
46
|
+
* DTCG dialect for dtcg/json outputs. 'legacy' (default): hex-string
|
|
47
|
+
* colors and bare-number dimensions, maximum compatibility (Style
|
|
48
|
+
* Dictionary v4, Tokens Studio). '2025': DTCG 2025.10 object colors
|
|
49
|
+
* ({ colorSpace, components, alpha?, hex }) and dimensions
|
|
50
|
+
* ({ value, unit }) for Style Dictionary v5+ and other 2025.10-aware
|
|
51
|
+
* tooling. Ignored by css/scss/tailwind/ts formatters, which render
|
|
52
|
+
* final code.
|
|
53
|
+
*/
|
|
54
|
+
dtcgDialect: z.enum(["legacy", "2025"]).optional(),
|
|
45
55
|
/** Per-target transform options. Override the global defaults. */
|
|
46
56
|
transforms: z
|
|
47
57
|
.object({
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTCG dialect helpers β legacy vs 2025.10 value encodings.
|
|
3
|
+
*
|
|
4
|
+
* The token pipeline historically speaks the "legacy" DTCG dialect: colors
|
|
5
|
+
* as hex strings ("#4085F2"), FLOAT dimensions as bare numbers. The DTCG
|
|
6
|
+
* 2025.10 spec (https://tr.designtokens.org/format/) switched to object
|
|
7
|
+
* forms:
|
|
8
|
+
*
|
|
9
|
+
* color = { colorSpace: "srgb", components: [r, g, b], alpha?, hex? }
|
|
10
|
+
* dimension = { value: number, unit: "px" | "rem" }
|
|
11
|
+
* duration = { value: number, unit: "ms" | "s" } (we already emit this)
|
|
12
|
+
*
|
|
13
|
+
* Export stays legacy by default (downstream consumers depend on it) and
|
|
14
|
+
* opts into 2025 via the `dtcgDialect` option. Import accepts BOTH dialects
|
|
15
|
+
* unconditionally. This module centralizes:
|
|
16
|
+
*
|
|
17
|
+
* - 2025 encoding helpers used by the dtcg/json formatters at render time
|
|
18
|
+
* - dialect-agnostic canonicalization used by the import diff so a 2025
|
|
19
|
+
* color object compares equal to the same color's legacy hex string
|
|
20
|
+
* (both are quantized to 1/255 per channel, tolerating the 8-bit
|
|
21
|
+
* precision loss inherent to hex)
|
|
22
|
+
* - stripping of the transient `rawColor` field the converter carries on
|
|
23
|
+
* COLOR TokenValues (full-precision floats for 2025 components) so it
|
|
24
|
+
* never leaks into serialized output
|
|
25
|
+
*/
|
|
26
|
+
export function clamp01(n) {
|
|
27
|
+
if (Number.isNaN(n))
|
|
28
|
+
return 0;
|
|
29
|
+
return Math.max(0, Math.min(1, n));
|
|
30
|
+
}
|
|
31
|
+
function byteHex(f) {
|
|
32
|
+
return Math.round(clamp01(f) * 255)
|
|
33
|
+
.toString(16)
|
|
34
|
+
.padStart(2, "0");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Canonicalize any color-like literal to a lowercase 8-digit hex string
|
|
38
|
+
* (`#rrggbbaa`), or return null when the literal isn't recognizably a color.
|
|
39
|
+
*
|
|
40
|
+
* Accepts:
|
|
41
|
+
* - hex strings: #rgb, #rrggbb, #rrggbbaa
|
|
42
|
+
* - 2025.10 color objects with srgb (or unspecified) colorSpace +
|
|
43
|
+
* 3 numeric components (+ optional alpha)
|
|
44
|
+
* - color objects with a hex fallback field (any colorSpace) + optional alpha
|
|
45
|
+
*
|
|
46
|
+
* Both the components form and the hex form quantize to 1 / 255 per channel,
|
|
47
|
+
* so a full-precision components array compares equal to the hex string the
|
|
48
|
+
* legacy pipeline derived from the same Figma floats.
|
|
49
|
+
*/
|
|
50
|
+
export function colorLiteralToCanonicalHex(literal) {
|
|
51
|
+
if (typeof literal === "string") {
|
|
52
|
+
const m = literal
|
|
53
|
+
.trim()
|
|
54
|
+
.match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
|
|
55
|
+
if (!m)
|
|
56
|
+
return null;
|
|
57
|
+
let digits = m[1].toLowerCase();
|
|
58
|
+
if (digits.length === 3) {
|
|
59
|
+
digits = digits
|
|
60
|
+
.split("")
|
|
61
|
+
.map((c) => c + c)
|
|
62
|
+
.join("");
|
|
63
|
+
}
|
|
64
|
+
if (digits.length === 6)
|
|
65
|
+
digits += "ff";
|
|
66
|
+
return `#${digits}`;
|
|
67
|
+
}
|
|
68
|
+
if (literal && typeof literal === "object" && !Array.isArray(literal)) {
|
|
69
|
+
const o = literal;
|
|
70
|
+
const colorSpace = typeof o.colorSpace === "string" ? o.colorSpace : undefined;
|
|
71
|
+
const comps = o.components;
|
|
72
|
+
if ((colorSpace === undefined || colorSpace === "srgb") &&
|
|
73
|
+
Array.isArray(comps) &&
|
|
74
|
+
comps.length === 3 &&
|
|
75
|
+
comps.every((c) => typeof c === "number")) {
|
|
76
|
+
const [r, g, b] = comps;
|
|
77
|
+
const a = typeof o.alpha === "number" ? o.alpha : 1;
|
|
78
|
+
return `#${byteHex(r)}${byteHex(g)}${byteHex(b)}${byteHex(a)}`;
|
|
79
|
+
}
|
|
80
|
+
if (typeof o.hex === "string") {
|
|
81
|
+
const base = colorLiteralToCanonicalHex(o.hex);
|
|
82
|
+
if (!base)
|
|
83
|
+
return null;
|
|
84
|
+
if (typeof o.alpha === "number") {
|
|
85
|
+
return base.slice(0, 7) + byteHex(o.alpha);
|
|
86
|
+
}
|
|
87
|
+
return base;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Parse a hex color string to raw rgba floats (0β1). Returns null on
|
|
94
|
+
* anything that isn't a valid 3/6/8-digit hex string.
|
|
95
|
+
*/
|
|
96
|
+
export function hexToRawRgba(hex) {
|
|
97
|
+
const canonical = colorLiteralToCanonicalHex(hex);
|
|
98
|
+
if (!canonical)
|
|
99
|
+
return null;
|
|
100
|
+
const d = canonical.slice(1);
|
|
101
|
+
return {
|
|
102
|
+
r: parseInt(d.slice(0, 2), 16) / 255,
|
|
103
|
+
g: parseInt(d.slice(2, 4), 16) / 255,
|
|
104
|
+
b: parseInt(d.slice(4, 6), 16) / 255,
|
|
105
|
+
a: parseInt(d.slice(6, 8), 16) / 255,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Encode a color TokenValue in the DTCG 2025.10 object form.
|
|
110
|
+
*
|
|
111
|
+
* Prefers the converter's transient `rawColor` floats (full precision β NOT
|
|
112
|
+
* round-tripped through 8-bit hex); falls back to parsing a hex-string
|
|
113
|
+
* literal. Literals already in object form pass through verbatim. Returns
|
|
114
|
+
* null when the value can't be encoded (caller keeps the legacy rendering).
|
|
115
|
+
*
|
|
116
|
+
* `alpha` is emitted only when < 1; `hex` is always included as the interop
|
|
117
|
+
* courtesy field (#RRGGBB, alpha carried separately).
|
|
118
|
+
*/
|
|
119
|
+
export function colorValueTo2025(value) {
|
|
120
|
+
const lit = value.literal;
|
|
121
|
+
if (lit && typeof lit === "object" && !Array.isArray(lit)) {
|
|
122
|
+
// Already object-form (e.g. a re-exported 2025 document) β pass through.
|
|
123
|
+
const o = lit;
|
|
124
|
+
if ("colorSpace" in o || "components" in o || "hex" in o)
|
|
125
|
+
return o;
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
let floats = value.rawColor ?? null;
|
|
129
|
+
if (!floats && typeof lit === "string") {
|
|
130
|
+
floats = hexToRawRgba(lit);
|
|
131
|
+
}
|
|
132
|
+
if (!floats)
|
|
133
|
+
return null;
|
|
134
|
+
const { r, g, b, a } = floats;
|
|
135
|
+
return {
|
|
136
|
+
colorSpace: "srgb",
|
|
137
|
+
components: [r, g, b],
|
|
138
|
+
...(a < 1 ? { alpha: a } : {}),
|
|
139
|
+
hex: `#${byteHex(r)}${byteHex(g)}${byteHex(b)}`.toUpperCase(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Encode a dimension literal in the DTCG 2025.10 object form. The converter
|
|
144
|
+
* emits Figma FLOAT dimensions as bare unitless numbers conventionally
|
|
145
|
+
* interpreted as px (the same convention the css/json formatters use), so
|
|
146
|
+
* only bare finite numbers are converted; everything else (unit strings,
|
|
147
|
+
* pre-encoded objects) keeps its current rendering.
|
|
148
|
+
*/
|
|
149
|
+
export function dimensionLiteralTo2025(literal) {
|
|
150
|
+
if (typeof literal === "number" && Number.isFinite(literal)) {
|
|
151
|
+
return { value: literal, unit: "px" };
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Normalize a TokenValue to a dialect-agnostic comparable form for diffing:
|
|
157
|
+
*
|
|
158
|
+
* - the transient `rawColor` field is dropped (Figma-side values carry it;
|
|
159
|
+
* parsed code-side values never do)
|
|
160
|
+
* - color-like literals (hex strings OR 2025 color objects) normalize to a
|
|
161
|
+
* lowercase 8-digit hex string, quantized to 1/255 per channel
|
|
162
|
+
* - `{ value, unit }` objects with px/ms normalize to the bare number
|
|
163
|
+
* ("s" converts to ms first), so `{value: 16, unit: "px"}` equals 16 and
|
|
164
|
+
* `{value: 0.3, unit: "s"}` equals `{value: 300, unit: "ms"}`
|
|
165
|
+
* - "16px"-style strings normalize to the bare number
|
|
166
|
+
*
|
|
167
|
+
* Conservative by design: anything not confidently recognized is returned
|
|
168
|
+
* unchanged (minus rawColor), falling back to the existing deep comparison.
|
|
169
|
+
*/
|
|
170
|
+
export function canonicalizeTokenValueForComparison(v) {
|
|
171
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
172
|
+
return v;
|
|
173
|
+
const obj = v;
|
|
174
|
+
if (!("literal" in obj) && !("reference" in obj))
|
|
175
|
+
return v;
|
|
176
|
+
const { rawColor: _rawColor, ...rest } = obj;
|
|
177
|
+
if (rest.literal !== undefined) {
|
|
178
|
+
const hex = colorLiteralToCanonicalHex(rest.literal);
|
|
179
|
+
if (hex !== null)
|
|
180
|
+
return { ...rest, literal: hex };
|
|
181
|
+
const num = numericLiteralToCanonical(rest.literal);
|
|
182
|
+
if (num !== null)
|
|
183
|
+
return { ...rest, literal: num };
|
|
184
|
+
}
|
|
185
|
+
return rest;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Normalize dimension/duration-shaped literals to a bare comparable number:
|
|
189
|
+
* - `{ value: n, unit: "px" | "ms" }` β n
|
|
190
|
+
* - `{ value: n, unit: "s" }` β n * 1000 (canonical ms)
|
|
191
|
+
* - `"16px"` strings β 16
|
|
192
|
+
* Returns null for everything else (including rem/em/% and objects with
|
|
193
|
+
* extra fields β those keep structural comparison).
|
|
194
|
+
*/
|
|
195
|
+
function numericLiteralToCanonical(literal) {
|
|
196
|
+
if (typeof literal === "string") {
|
|
197
|
+
const m = literal.trim().match(/^(-?(?:\d+\.?\d*|\.\d+))px$/i);
|
|
198
|
+
return m ? Number(m[1]) : null;
|
|
199
|
+
}
|
|
200
|
+
if (literal &&
|
|
201
|
+
typeof literal === "object" &&
|
|
202
|
+
!Array.isArray(literal) &&
|
|
203
|
+
Object.keys(literal).length === 2) {
|
|
204
|
+
const o = literal;
|
|
205
|
+
if (typeof o.value === "number" && typeof o.unit === "string") {
|
|
206
|
+
if (o.unit === "px" || o.unit === "ms")
|
|
207
|
+
return o.value;
|
|
208
|
+
if (o.unit === "s")
|
|
209
|
+
return o.value * 1000;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Return a copy of a mode-keyed TokenValue map with the transient `rawColor`
|
|
216
|
+
* field removed from every entry. Used wherever values get serialized
|
|
217
|
+
* (lastSyncedValue snapshots, diff-plan samples) so the transient field never
|
|
218
|
+
* appears in output β keeping legacy output byte-identical.
|
|
219
|
+
*/
|
|
220
|
+
export function stripRawColorFromValues(values) {
|
|
221
|
+
const out = {};
|
|
222
|
+
for (const [k, v] of Object.entries(values)) {
|
|
223
|
+
if (v && typeof v === "object" && "rawColor" in v) {
|
|
224
|
+
const { rawColor: _rawColor, ...rest } = v;
|
|
225
|
+
out[k] = rest;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
out[k] = v;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* $extensions["figma-console-mcp"] for round-trip non-destructiveness.
|
|
13
13
|
*/
|
|
14
14
|
import { slugifySetName } from "./alias-resolver.js";
|
|
15
|
+
import { stripRawColorFromValues } from "./dialect.js";
|
|
15
16
|
/**
|
|
16
17
|
* Convert a Figma variables payload to our canonical TokenDocument.
|
|
17
18
|
*/
|
|
@@ -126,14 +127,35 @@ function convertVariable(variable, wantedModes, variableById, collectionNameById
|
|
|
126
127
|
// reconstruct it.
|
|
127
128
|
...(strippedTypeSuffix ? { figmaName: variable.name } : {}),
|
|
128
129
|
...(Object.keys(springByMode).length > 0 ? { spring: springByMode } : {}),
|
|
130
|
+
// Scopes: stash only when NON-default. ["ALL_SCOPES"] (Figma's
|
|
131
|
+
// default) and empty/absent arrays are omitted so pre-existing
|
|
132
|
+
// exports stay byte-identical. codeSyntax: stash only when
|
|
133
|
+
// non-empty, same reasoning.
|
|
134
|
+
...(hasNonDefaultScopes(variable.scopes)
|
|
135
|
+
? { scopes: [...variable.scopes] }
|
|
136
|
+
: {}),
|
|
137
|
+
...(variable.codeSyntax && Object.keys(variable.codeSyntax).length > 0
|
|
138
|
+
? { codeSyntax: { ...variable.codeSyntax } }
|
|
139
|
+
: {}),
|
|
129
140
|
lastSyncedAt: new Date().toISOString(),
|
|
130
141
|
// We snapshot the synced value so future merge calls can detect
|
|
131
|
-
// two-sided conflicts.
|
|
132
|
-
|
|
142
|
+
// two-sided conflicts. rawColor is transient render-time data and
|
|
143
|
+
// must not leak into serialized extensions (legacy output stays
|
|
144
|
+
// byte-identical).
|
|
145
|
+
lastSyncedValue: stripRawColorFromValues({ ...values }),
|
|
133
146
|
},
|
|
134
147
|
},
|
|
135
148
|
};
|
|
136
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* True when a variable's scopes array is meaningfully restrictive β i.e.
|
|
152
|
+
* present, non-empty, and not just the default ["ALL_SCOPES"].
|
|
153
|
+
*/
|
|
154
|
+
function hasNonDefaultScopes(scopes) {
|
|
155
|
+
if (!Array.isArray(scopes) || scopes.length === 0)
|
|
156
|
+
return false;
|
|
157
|
+
return !(scopes.length === 1 && scopes[0] === "ALL_SCOPES");
|
|
158
|
+
}
|
|
137
159
|
function mapResolvedType(resolvedType, variableName, warnings) {
|
|
138
160
|
switch (resolvedType) {
|
|
139
161
|
case "COLOR":
|
|
@@ -212,7 +234,19 @@ function convertValue(rawValue, variable, modeName, variableById, collectionName
|
|
|
212
234
|
// Literal values per type.
|
|
213
235
|
if (resolvedType === "COLOR") {
|
|
214
236
|
if (typeof rawValue === "object" && rawValue !== null && "r" in rawValue) {
|
|
215
|
-
|
|
237
|
+
// The hex string stays the literal (legacy dialect + back-compat), but
|
|
238
|
+
// we also carry the raw full-precision floats so the 2025.10 dialect
|
|
239
|
+
// can emit `components` without round-tripping through 8-bit hex.
|
|
240
|
+
// `rawColor` is transient β see TokenValue.rawColor in types.ts.
|
|
241
|
+
return {
|
|
242
|
+
literal: rgbaToHex(rawValue),
|
|
243
|
+
rawColor: {
|
|
244
|
+
r: rawValue.r,
|
|
245
|
+
g: rawValue.g,
|
|
246
|
+
b: rawValue.b,
|
|
247
|
+
a: rawValue.a ?? 1,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
216
250
|
}
|
|
217
251
|
warnings.push(`COLOR value isn't an RGB object: ${JSON.stringify(rawValue)}`);
|
|
218
252
|
return { literal: String(rawValue) };
|