figma-rust-mcp 3.2.5 → 3.2.8
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 +77 -5
- package/bin/figma-mcp.js +7 -4
- package/package.json +3 -2
- package/plugin/code.js +1 -1
- package/scripts/build-plugin.js +21 -4
- package/scripts/test-plugin-tokens.mjs +147 -0
- package/scripts/test-rust-mcp.sh +2 -2
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
28
28
|
┌────────────────────────────────────────────────────────────────────────┐
|
|
29
29
|
│ Terminal / Background Service │
|
|
30
30
|
│ figma-mcp (Pure Rust Engine) │
|
|
31
|
-
│ • MCP
|
|
31
|
+
│ • MCP Streamable HTTP (/mcp) & legacy SSE (/sse, /message) │
|
|
32
32
|
│ • Dynamic Runtime Server (/plugin/code.js, /plugin/ui.html) │
|
|
33
33
|
│ • In-Memory Fast Index (<1ms Lookups & Incremental Diffs) │
|
|
34
34
|
│ • Design-to-Code Compiler (React/Tailwind, Vue, RN, SwiftUI) │
|
|
@@ -36,8 +36,8 @@ Enables AI agents (Google Antigravity, Claude Code, Cursor, Windsurf, VS Code, Z
|
|
|
36
36
|
│ • Sandboxed JS Runtime (Boa ECMAScript Engine) │
|
|
37
37
|
│ • Binary MessagePack & Progressive Subtree Chunk Receiver │
|
|
38
38
|
└───────────────▲────────────────────────────────────────▲───────────────┘
|
|
39
|
-
│ (ws://127.0.0.1:38451/ws) │ (http://127.0.0.1:38451/
|
|
40
|
-
│ (Dynamic Code Streaming & Hot-Reload) │ (JSON-RPC 2.0 /
|
|
39
|
+
│ (ws://127.0.0.1:38451/ws) │ (http://127.0.0.1:38451/mcp)
|
|
40
|
+
│ (Dynamic Code Streaming & Hot-Reload) │ (JSON-RPC 2.0 / Streamable HTTP)
|
|
41
41
|
┌─────────┴─────────┐ ┌─────────┴─────────┐
|
|
42
42
|
│ Figma Desktop │ │ AI Assistant(s) │
|
|
43
43
|
│ (Thin Loader) │ │ Google Antigravity│
|
|
@@ -107,6 +107,25 @@ npx -y figma-rust-mcp@latest --uninstall-service
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
+
### Windows background mode
|
|
111
|
+
|
|
112
|
+
Double-clicking `figma-mcp.exe` starts the server in a detached process and closes
|
|
113
|
+
its launcher console. From PowerShell or Command Prompt, use:
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
.\figma-mcp.exe --background
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The server keeps running after the terminal closes. Logs are appended to
|
|
120
|
+
`%LOCALAPPDATA%\figma-mcp\logs\server.log`. Use `--server` for the foreground
|
|
121
|
+
terminal dashboard, or `--stdio` for an MCP client subprocess.
|
|
122
|
+
|
|
123
|
+
`--install-service` through the npm runner configures background startup at login.
|
|
124
|
+
Re-run the installation command to update an existing Windows task. Removing the
|
|
125
|
+
task disables future login startup; stop an already detached server through Task
|
|
126
|
+
Manager (`figma-mcp.exe`). The launcher console may appear briefly when opening
|
|
127
|
+
the executable; the server itself does not retain a console window.
|
|
128
|
+
|
|
110
129
|
### 4. Build from Source (Optional)
|
|
111
130
|
|
|
112
131
|
If you prefer building directly with the [Rust toolchain](https://rustup.rs/) (`cargo >= 1.80`):
|
|
@@ -122,7 +141,22 @@ cargo build --release
|
|
|
122
141
|
|
|
123
142
|
### 5. Configure Your MCP Client
|
|
124
143
|
|
|
125
|
-
####
|
|
144
|
+
#### Codex and clients using Streamable HTTP (Recommended)
|
|
145
|
+
|
|
146
|
+
Use the `/mcp` endpoint. It accepts `POST` JSON-RPC requests directly:
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[mcp_servers.figma-mcp]
|
|
150
|
+
url = "http://127.0.0.1:38451/mcp"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For Codex CLI, the equivalent command is:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
codex mcp add figma-mcp --url http://127.0.0.1:38451/mcp
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Google Antigravity (SSE Transport)
|
|
126
160
|
Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.json`:
|
|
127
161
|
```json
|
|
128
162
|
{
|
|
@@ -136,7 +170,7 @@ Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.js
|
|
|
136
170
|
|
|
137
171
|
#### Claude Code / Cursor / Windsurf / VS Code / Zed
|
|
138
172
|
|
|
139
|
-
**Option A: SSE Transport (
|
|
173
|
+
**Option A: SSE Transport (for clients that require SSE)**
|
|
140
174
|
```json
|
|
141
175
|
{
|
|
142
176
|
"mcpServers": {
|
|
@@ -147,6 +181,11 @@ Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.js
|
|
|
147
181
|
}
|
|
148
182
|
```
|
|
149
183
|
|
|
184
|
+
The SSE transport uses `GET /sse` to open the event stream and `POST /message`
|
|
185
|
+
(or `/messages`) for JSON-RPC messages. Do not configure a Streamable HTTP
|
|
186
|
+
client such as Codex against `/sse`: that endpoint does not accept `POST`, so
|
|
187
|
+
the initialize request will return HTTP 405. Use `/mcp` instead.
|
|
188
|
+
|
|
150
189
|
**Option B: Stdio Subprocess (via NPX)**
|
|
151
190
|
```json
|
|
152
191
|
{
|
|
@@ -159,6 +198,18 @@ Add to your `~/.gemini/config/mcp_config.json` or project `.agents/mcp_config.js
|
|
|
159
198
|
}
|
|
160
199
|
```
|
|
161
200
|
|
|
201
|
+
### 6. Diagnose a stale Figma runtime
|
|
202
|
+
|
|
203
|
+
Check the runtime bundle currently served to Figma Desktop:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
curl http://127.0.0.1:38451/plugin/version
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The response includes `version` and `runtimeHash`. If the hash changes after an
|
|
210
|
+
upgrade, the dynamic plugin runtime is refreshed automatically. Restart the
|
|
211
|
+
Figma plugin only if the reported hash does not change.
|
|
212
|
+
|
|
162
213
|
---
|
|
163
214
|
|
|
164
215
|
## 🛠️ MCP Tools Reference
|
|
@@ -183,6 +234,27 @@ Exports Figma Variables (Design Tokens), Color Styles, Typography Styles, and El
|
|
|
183
234
|
- **`outputPath`**: Writes directly to disk (e.g. `src/styles/tokens.css` or `tailwind.config.js`).
|
|
184
235
|
- **`collection`** / **`mode`** / **`prefix`**: Optional filters and naming prefixes.
|
|
185
236
|
|
|
237
|
+
Token exports use schema v2 and preserve full paint/effect stacks, alpha, gradient
|
|
238
|
+
transforms, collection modes and raw alias values. CSS, Tailwind, TypeScript and
|
|
239
|
+
JSON share one normalized model. The `w3c` output uses DTCG 2025.10 color,
|
|
240
|
+
dimension, gradient-stop and shadow values, with Figma-specific geometry and
|
|
241
|
+
unsupported values retained in `$extensions`.
|
|
242
|
+
|
|
243
|
+
Linear gradients on a node are projected using its actual dimensions. A diagonal
|
|
244
|
+
gradient style has no target aspect ratio: use `get_css` on its consuming node.
|
|
245
|
+
Radial/angular/diamond paints, unsupported blend modes and progressive blurs
|
|
246
|
+
retain their raw data and report diagnostics recommending SVG instead of emitting
|
|
247
|
+
approximate CSS. Unknown modes, unresolved aliases and sanitized name collisions
|
|
248
|
+
fail explicitly. CSS exports include non-default modes under `data-theme`
|
|
249
|
+
selectors; opacity/font-weight variables stay unitless.
|
|
250
|
+
|
|
251
|
+
The plugin UI badge is stamped from the build version and updated from the
|
|
252
|
+
connected server handshake. Package/Cargo/tag mismatches fail the build pipeline.
|
|
253
|
+
Run `npm run build:plugin` after changing plugin source, and `npm run test:tokens`
|
|
254
|
+
for the token/contract/version regression suite. Existing background servers need
|
|
255
|
+
a restart with the new binary to load the updated runtime.
|
|
256
|
+
|
|
257
|
+
|
|
186
258
|
### 5. `figma_get_selection`
|
|
187
259
|
Token-compressed inspection of currently selected layers/frames on the Figma canvas. Returns compacted layout and typography hierarchy with **60–80% fewer tokens**.
|
|
188
260
|
- **`detail`**: `"compact"` (recommended), `"minimal"`, `"full"`.
|
package/bin/figma-mcp.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* caches it locally, and executes it transparently.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { spawn, execSync } from 'node:child_process';
|
|
9
|
+
import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import os from 'node:os';
|
|
@@ -19,7 +19,7 @@ const __dirname = path.dirname(__filename);
|
|
|
19
19
|
|
|
20
20
|
// Read package.json version
|
|
21
21
|
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
22
|
-
let pkgVersion =
|
|
22
|
+
let pkgVersion = null;
|
|
23
23
|
let repoOwner = 'BuiHung1612';
|
|
24
24
|
let repoName = 'figma-mcp';
|
|
25
25
|
|
|
@@ -36,6 +36,7 @@ try {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
} catch (_) {}
|
|
39
|
+
if (!pkgVersion) throw new Error(`Cannot read package version from ${pkgPath}`);
|
|
39
40
|
|
|
40
41
|
function getPlatformArch() {
|
|
41
42
|
const platform = process.platform;
|
|
@@ -293,8 +294,10 @@ WantedBy=default.target
|
|
|
293
294
|
} else if (platform === 'win32') {
|
|
294
295
|
const taskName = 'FigmaMCPServer';
|
|
295
296
|
// Use schtasks to create a task that runs at login
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
// The native launcher detaches the server and redirects output to a log file.
|
|
298
|
+
const taskCommand = `"${binPath}" --background`;
|
|
299
|
+
execFileSync('schtasks', ['/Create', '/F', '/TN', taskName,
|
|
300
|
+
'/TR', taskCommand, '/SC', 'ONLOGON'], { stdio: 'inherit', windowsHide: true });
|
|
298
301
|
|
|
299
302
|
// Start it immediately too
|
|
300
303
|
try { execSync(`schtasks /Run /TN "${taskName}"`, { stdio: 'pipe' }); } catch (_) {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-rust-mcp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.8",
|
|
4
4
|
"description": "Claude Code to Figma — Bidirectional MCP bridge. AI draws UI on Figma canvas, reads designs back. Works with Claude, Cursor, VS Code, Windsurf, Antigravity. No API key needed.",
|
|
5
5
|
"mcpName": "io.github.BuiHung1612/figma-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "cargo build --release",
|
|
19
|
+
"build": "npm run build:plugin && cargo build --release",
|
|
20
20
|
"build:plugin": "node scripts/build-plugin.js",
|
|
21
21
|
"test": "./scripts/test-rust-mcp.sh",
|
|
22
|
+
"test:tokens": "node --test scripts/test-plugin-tokens.mjs && cargo test",
|
|
22
23
|
"dev": "cargo run"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
package/plugin/code.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ─── FIGMA MCP BRIDGE — DYNAMIC THIN LOADER
|
|
1
|
+
// ─── FIGMA MCP BRIDGE — DYNAMIC THIN LOADER ───────────────────────────
|
|
2
2
|
// This file is a permanent micro-stub. You never need to reinstall or edit this file.
|
|
3
3
|
// The actual runtime is loaded dynamically from the local figma-mcp Rust engine.
|
|
4
4
|
|
package/scripts/build-plugin.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// Build script — concatenate src
|
|
1
|
+
// Build script — concatenate plugin-src/*.js into plugin-runtime/code.js
|
|
2
2
|
// No module system used: files are plain JS sharing a single scope when concatenated.
|
|
3
3
|
import { readFileSync, writeFileSync } from "fs";
|
|
4
4
|
|
|
5
5
|
const { version } = JSON.parse(readFileSync("package.json", "utf8"));
|
|
6
|
+
const cargoVersion = readFileSync("Cargo.toml", "utf8").match(/^version\s*=\s*"([^"]+)"/m)?.[1];
|
|
7
|
+
if (version !== cargoVersion) throw new Error(`Version mismatch: package.json ${version}, Cargo.toml ${cargoVersion}`);
|
|
8
|
+
if (process.env.GITHUB_REF_TYPE === "tag" && process.env.GITHUB_REF_NAME !== `v${version}`) {
|
|
9
|
+
throw new Error(`Release tag ${process.env.GITHUB_REF_NAME} does not match build version v${version}`);
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
const files = [
|
|
8
13
|
"plugin-src/utils.js",
|
|
9
14
|
"plugin-src/svg-path-helpers.js",
|
|
10
15
|
"plugin-src/paint-and-effects.js",
|
|
16
|
+
"plugin-src/token-helpers.js",
|
|
11
17
|
"plugin-src/read-helpers.js",
|
|
12
18
|
"plugin-src/handlers-write.js",
|
|
13
19
|
"plugin-src/handlers-library.js",
|
|
@@ -18,10 +24,9 @@ const files = [
|
|
|
18
24
|
"plugin-src/main.js",
|
|
19
25
|
];
|
|
20
26
|
|
|
21
|
-
const banner = `// figma-mcp — Figma Plugin main thread
|
|
27
|
+
const banner = `// figma-mcp — Figma Plugin main thread v${version}
|
|
22
28
|
// Handles both WRITE (draw UI) and READ (extract design) operations.
|
|
23
29
|
// AUTO-GENERATED by scripts/build-plugin.js — edit plugin-src/*.js instead
|
|
24
|
-
// Generated: ${new Date().toISOString()}
|
|
25
30
|
`;
|
|
26
31
|
|
|
27
32
|
const parts = files.map(f => {
|
|
@@ -30,5 +35,17 @@ const parts = files.map(f => {
|
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
const output = (banner + parts.join("\n")).replace(/\{\{PLUGIN_VERSION\}\}/g, version);
|
|
33
|
-
|
|
38
|
+
|
|
39
|
+
const uiPath = "plugin-runtime/ui.html";
|
|
40
|
+
const ui = readFileSync(uiPath, "utf8").replace(/(<span class="brand-ver"[^>]*>)[^<]*(<\/span>)/,
|
|
41
|
+
`$1v${version}$2`);
|
|
42
|
+
if (!ui.includes(`id="runtime-version">v${version}</span>`)) throw new Error("Missing runtime version badge");
|
|
43
|
+
if (process.argv.includes('--check')) {
|
|
44
|
+
if (readFileSync("plugin-runtime/code.js", "utf8") !== output || readFileSync(uiPath, "utf8") !== ui) {
|
|
45
|
+
throw new Error("Plugin bundle is stale; run npm run build:plugin");
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
writeFileSync("plugin-runtime/code.js", output);
|
|
49
|
+
writeFileSync(uiPath, ui);
|
|
50
|
+
}
|
|
34
51
|
console.log(`✓ plugin-runtime/code.js written (${output.length} chars, ${output.split("\n").length} lines)`);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import vm from 'node:vm';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
function runtime(figma = {}) {
|
|
8
|
+
const context = vm.createContext({ figma, handlers: {}, console, setTimeout, Map, Set, Uint8Array });
|
|
9
|
+
for (const file of ['utils', 'token-helpers', 'handlers-read-detail', 'handlers-read', 'handlers-tokens', 'handlers-write-ops']) {
|
|
10
|
+
vm.runInContext(readFileSync(`plugin-src/${file}.js`, 'utf8'), context, { filename: file });
|
|
11
|
+
}
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
14
|
+
const plain = value => JSON.parse(JSON.stringify(value));
|
|
15
|
+
const gradient = (transform = [[1, 0, 0], [0, 1, 0]]) => ({
|
|
16
|
+
type: 'GRADIENT_LINEAR', gradientTransform: transform, opacity: .5,
|
|
17
|
+
gradientStops: [{ position: 0, color: { r: 1, g: 0, b: 0, a: .5 } }, { position: 1, color: { r: 0, g: 0, b: 1, a: 1 } }],
|
|
18
|
+
});
|
|
19
|
+
const shadow = type => ({ type, color: { r: 0, g: 0, b: 0, a: .125 }, offset: { x: -1, y: 3 }, radius: 6, spread: -2, visible: true });
|
|
20
|
+
|
|
21
|
+
test('equivalent CSS syntaxes preserve alpha in reads and writes', () => {
|
|
22
|
+
const r = runtime();
|
|
23
|
+
for (const color of ['rgba(255, 0, 0, .5)', 'rgb(100% 0% 0% / 50%)', 'hsl(0 100% 50% / .5)', 'hsla(0, 100%, 50%, .5)']) {
|
|
24
|
+
assert.deepEqual(plain(r.parseColorValue(color)), { r: 1, g: 0, b: 0, a: .5 });
|
|
25
|
+
assert.equal(r.hexToRgbA(color).a, .5);
|
|
26
|
+
assert.equal(r.solidFill(color)[0].opacity, .5);
|
|
27
|
+
}
|
|
28
|
+
assert.equal(r.parseColorValue('#f008').a, 136 / 255);
|
|
29
|
+
assert.equal(r.parseColorValue('transparent').a, 0);
|
|
30
|
+
assert.equal(r.colorToCss({ r: 0, g: 0, b: 0, a: .9999 }), 'rgba(0, 0, 0, 0.9999)');
|
|
31
|
+
assert.throws(() => r.parseColorValue('rgba(garbage)'), /Invalid/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('gradient projection includes translations, aspect ratio, stop and paint alpha', () => {
|
|
35
|
+
const r = runtime();
|
|
36
|
+
const p = r.serializePaint(gradient());
|
|
37
|
+
assert.deepEqual(plain(p.gradientTransform), [[1, 0, 0], [0, 1, 0]]);
|
|
38
|
+
assert.match(r.paintToCss(p, 200, 100), /^linear-gradient\(90deg, rgba\(255, 0, 0, 0.25\) 0%, rgba\(0, 0, 255, 0.5\) 100%\)$/);
|
|
39
|
+
const diagonal = r.paintToCss(r.serializePaint(gradient([[1, 1, 0], [0, 1, 0]])), 200, 100);
|
|
40
|
+
assert.match(diagonal, /153\.434948/);
|
|
41
|
+
assert.match(diagonal, /50%/);
|
|
42
|
+
const shifted = r.paintToCss(r.serializePaint(gradient([[.5, 0, .25], [0, 1, 0]])), 200, 100);
|
|
43
|
+
assert.match(shifted, /-50%/);
|
|
44
|
+
assert.match(shifted, /150%/);
|
|
45
|
+
assert.throws(() => r.paintToCss({ ...p, type: 'GRADIENT_DIAMOND' }, 100, 100), /SVG/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('get_css uses the numeric paint, never hex slicing a serialized rgba', async () => {
|
|
49
|
+
const node = { id: '1:2', name: 'alpha', type: 'RECTANGLE', x: 0, y: 0, width: 200, height: 100,
|
|
50
|
+
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 }, opacity: .25 }], effects: [shadow('DROP_SHADOW'), shadow('INNER_SHADOW')] };
|
|
51
|
+
const r = runtime();
|
|
52
|
+
r.findNodeByIdAsync = async () => node;
|
|
53
|
+
const result = await r.handlers.get_css({ id: node.id });
|
|
54
|
+
assert.match(result.css, /background-color: rgba\(255, 0, 0, 0.25\);/);
|
|
55
|
+
assert.doesNotMatch(result.css, /NaN|0.0625/);
|
|
56
|
+
assert.match(result.css, /inset -1px 3px 6px -2px rgba\(0, 0, 0, 0.125\)/);
|
|
57
|
+
assert.equal(result.detail.effects.length, 2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('styles retain full paints, effects, units and disabled layers', async () => {
|
|
61
|
+
const r = runtime({
|
|
62
|
+
getLocalPaintStylesAsync: async () => [{ id: 'p', name: 'Gradient', paints: [gradient()] }],
|
|
63
|
+
getLocalTextStylesAsync: async () => [{ id: 't', name: 'Body', fontName: { family: 'Inter', style: 'Regular' }, fontSize: 16, lineHeight: { unit: 'PERCENT', value: 150 }, letterSpacing: { unit: 'PERCENT', value: 2 } }],
|
|
64
|
+
getLocalEffectStylesAsync: async () => [{ id: 's', name: 'Card', effects: [shadow('DROP_SHADOW'), shadow('INNER_SHADOW'), { type: 'LAYER_BLUR', radius: 8, visible: false }] }],
|
|
65
|
+
getLocalGridStylesAsync: async () => [],
|
|
66
|
+
});
|
|
67
|
+
const styles = await r.handlers.get_styles();
|
|
68
|
+
assert.equal(styles.schemaVersion, 2);
|
|
69
|
+
assert.equal(styles.paintStyles[0].paints[0].gradientStops[0].rgba.a, .5);
|
|
70
|
+
assert.equal(styles.effectStyles[0].effects.length, 3);
|
|
71
|
+
assert.equal(styles.textStyles[0].lineHeightUnit, 'PERCENT');
|
|
72
|
+
assert.equal(r.effectsToCss(styles.effectStyles[0].effects).filter, '');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('aliases use mode names across collections and retain false/zero values', async () => {
|
|
76
|
+
const cols = [
|
|
77
|
+
{ id: 'semantic', name: 'Semantic', defaultModeId: 's-light', modes: [{ modeId: 's-light', name: 'Light' }, { modeId: 's-dark', name: 'Dark' }], variableIds: ['alias', 'zero'] },
|
|
78
|
+
{ id: 'primitive', name: 'Primitive', defaultModeId: 'p-light', modes: [{ modeId: 'p-light', name: 'Light' }, { modeId: 'p-dark', name: 'Dark' }], variableIds: ['base'] },
|
|
79
|
+
];
|
|
80
|
+
const variables = [
|
|
81
|
+
{ id: 'alias', name: 'semantic', variableCollectionId: 'semantic', resolvedType: 'BOOLEAN', valuesByMode: { 's-light': { type: 'VARIABLE_ALIAS', id: 'base' }, 's-dark': { type: 'VARIABLE_ALIAS', id: 'base' } } },
|
|
82
|
+
{ id: 'zero', name: 'zero', variableCollectionId: 'semantic', resolvedType: 'FLOAT', valuesByMode: { 's-light': 0, 's-dark': 1 } },
|
|
83
|
+
{ id: 'base', name: 'base', variableCollectionId: 'primitive', resolvedType: 'BOOLEAN', valuesByMode: { 'p-light': false, 'p-dark': true } },
|
|
84
|
+
];
|
|
85
|
+
const r = runtime({ variables: {
|
|
86
|
+
getLocalVariableCollectionsAsync: async () => cols, getLocalVariablesAsync: async () => variables,
|
|
87
|
+
getVariableByIdAsync: async id => variables.find(v => v.id === id),
|
|
88
|
+
getVariableCollectionByIdAsync: async id => cols.find(c => c.id === id),
|
|
89
|
+
} });
|
|
90
|
+
const result = await r.handlers.get_variables();
|
|
91
|
+
const alias = result.collections[0].variables[0];
|
|
92
|
+
assert.equal(alias.values['s-light'].resolvedValue, false);
|
|
93
|
+
assert.equal(alias.values['s-dark'].resolvedValue, true);
|
|
94
|
+
assert.equal(result.resolvedTokens.semantic, false);
|
|
95
|
+
assert.equal(result.resolvedTokens.zero, 0);
|
|
96
|
+
assert.equal(result.collections[0].defaultModeId, 's-light');
|
|
97
|
+
assert.deepEqual(plain(result.diagnostics), []);
|
|
98
|
+
const consumer = { resolvedVariableModes: { semantic: 's-dark', primitive: 'p-light' } };
|
|
99
|
+
variables[0].resolveForConsumer = () => ({ value: false, resolvedType: 'BOOLEAN' });
|
|
100
|
+
assert.equal((await r.resolveVariableValueAsync(variables[0], consumer)).resolvedValue, false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('export_node validates format and works through batch and aliases', async () => {
|
|
104
|
+
const r = runtime();
|
|
105
|
+
r.handlers.export_svg = async () => ({ svg: '<svg/>' });
|
|
106
|
+
r.handlers.export_image = async params => ({ format: params.format });
|
|
107
|
+
assert.equal((await r.resolveOperationHandler('exportNode')({ format: 'svg' })).svg, '<svg/>');
|
|
108
|
+
assert.equal((await r.handlers.export_node({ format: 'jpeg' })).format, 'JPG');
|
|
109
|
+
const batch = await r.handlers.batch({ operations: [{ operation: 'export_node', params: { format: 'SVG' } }, { operation: 'export_node', params: { format: 'bad' } }] });
|
|
110
|
+
assert.equal(batch.succeeded, 1);
|
|
111
|
+
assert.match(batch.results[1].error, /supports PNG/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('multiple paints preserve top-first ordering and node opacity stays separate', () => {
|
|
115
|
+
const r = runtime();
|
|
116
|
+
const paints = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 }, opacity: .5 }, { type: 'SOLID', color: { r: 0, g: 0, b: 1 }, opacity: 1 }].map(r.serializePaint);
|
|
117
|
+
const css = r.paintsToCss(paints, 200, 100);
|
|
118
|
+
assert.match(css.value, /^linear-gradient\(rgba\(255, 0, 0, 0.5\)/);
|
|
119
|
+
assert.match(css.value, /linear-gradient\(#0000ff, #0000ff\)$/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('UI badge uses release version initially and follows the server on reconnect', () => {
|
|
123
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
124
|
+
const html = readFileSync('plugin-runtime/ui.html', 'utf8');
|
|
125
|
+
assert.ok(html.includes(`id="runtime-version">v${pkg.version}</span>`));
|
|
126
|
+
const start = html.indexOf(' function updateRuntimeVersion(');
|
|
127
|
+
const end = html.indexOf(' var runtimeCapabilities', start);
|
|
128
|
+
const badge = {};
|
|
129
|
+
const context = vm.createContext({ document: { getElementById: () => badge } });
|
|
130
|
+
vm.runInContext(html.slice(start, end), context);
|
|
131
|
+
context.updateRuntimeVersion('3.2.7');
|
|
132
|
+
assert.equal(badge.textContent, 'v3.2.7');
|
|
133
|
+
context.updateRuntimeVersion('3.3.0');
|
|
134
|
+
assert.equal(badge.textContent, 'v3.3.0');
|
|
135
|
+
assert.ok(html.includes('updateRuntimeVersion(serverVer)'));
|
|
136
|
+
assert.ok(html.includes('type: "runtime-ready"'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('build rejects wrong release tags and checked-in bundle matches source', () => {
|
|
140
|
+
const check = spawnSync(process.execPath, ['scripts/build-plugin.js', '--check'], { encoding: 'utf8' });
|
|
141
|
+
assert.equal(check.status, 0, check.stderr);
|
|
142
|
+
const rejected = spawnSync(process.execPath, ['scripts/build-plugin.js'], {
|
|
143
|
+
encoding: 'utf8', env: { ...process.env, GITHUB_REF_TYPE: 'tag', GITHUB_REF_NAME: 'v0.0.0-invalid' },
|
|
144
|
+
});
|
|
145
|
+
assert.notEqual(rejected.status, 0);
|
|
146
|
+
assert.match(rejected.stderr, /does not match build version/);
|
|
147
|
+
});
|
package/scripts/test-rust-mcp.sh
CHANGED
|
@@ -26,8 +26,8 @@ OUTPUT=$(echo "$INPUT" | $BIN_PATH --port 38499 2>/dev/null)
|
|
|
26
26
|
echo "$OUTPUT" | head -n 4
|
|
27
27
|
|
|
28
28
|
# Verify tool registration in JSON-RPC output
|
|
29
|
-
echo "--- Test 3: Verify Tool Registrations (
|
|
30
|
-
for TOOL in figma_status figma_get_selection figma_inspect_node figma_to_code figma_get_tokens figma_export_asset figma_export_assets figma_index figma_read figma_write figma_rules figma_docs; do
|
|
29
|
+
echo "--- Test 3: Verify Tool Registrations (15 tools) ---"
|
|
30
|
+
for TOOL in figma_status figma_get_selection figma_inspect_node figma_to_code figma_get_tokens figma_export_asset figma_export_assets figma_index figma_read figma_write figma_rules figma_docs figma_match_components figma_prepare_design figma_verify_ui; do
|
|
31
31
|
echo "$OUTPUT" | grep -q "\"name\":\"$TOOL\"" && echo "✓ $TOOL registered"
|
|
32
32
|
done
|
|
33
33
|
|