@mp3wizard/figma-console-mcp 1.14.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/LICENSE +21 -0
- package/README.md +816 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
- package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
- package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
- package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
- package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
- package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
- package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
- package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
- package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
- package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
- package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
- package/dist/apps/design-system-dashboard/server.d.ts +24 -0
- package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
- package/dist/apps/design-system-dashboard/server.js +160 -0
- package/dist/apps/design-system-dashboard/server.js.map +1 -0
- package/dist/apps/token-browser/server.d.ts +26 -0
- package/dist/apps/token-browser/server.d.ts.map +1 -0
- package/dist/apps/token-browser/server.js +137 -0
- package/dist/apps/token-browser/server.js.map +1 -0
- package/dist/browser/base.d.ts +58 -0
- package/dist/browser/base.d.ts.map +1 -0
- package/dist/browser/base.js +6 -0
- package/dist/browser/base.js.map +1 -0
- package/dist/browser/local.d.ts +87 -0
- package/dist/browser/local.d.ts.map +1 -0
- package/dist/browser/local.js +318 -0
- package/dist/browser/local.js.map +1 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
- package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
- package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
- package/dist/cloudflare/apps/token-browser/server.js +136 -0
- package/dist/cloudflare/browser/base.js +5 -0
- package/dist/cloudflare/browser/cloudflare.js +156 -0
- package/dist/cloudflare/browser-manager.js +157 -0
- package/dist/cloudflare/core/cloud-websocket-connector.js +267 -0
- package/dist/cloudflare/core/cloud-websocket-relay.js +199 -0
- package/dist/cloudflare/core/comment-tools.js +292 -0
- package/dist/cloudflare/core/config.js +161 -0
- package/dist/cloudflare/core/console-monitor.js +427 -0
- package/dist/cloudflare/core/design-code-tools.js +2504 -0
- package/dist/cloudflare/core/design-system-manifest.js +260 -0
- package/dist/cloudflare/core/design-system-tools.js +863 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +272 -0
- package/dist/cloudflare/core/enrichment/index.js +7 -0
- package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
- package/dist/cloudflare/core/enrichment/style-resolver.js +326 -0
- package/dist/cloudflare/core/figma-api.js +409 -0
- package/dist/cloudflare/core/figma-connector.js +7 -0
- package/dist/cloudflare/core/figma-desktop-connector.js +1184 -0
- package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
- package/dist/cloudflare/core/figma-style-extractor.js +311 -0
- package/dist/cloudflare/core/figma-tools.js +2947 -0
- package/dist/cloudflare/core/logger.js +53 -0
- package/dist/cloudflare/core/port-discovery.js +282 -0
- package/dist/cloudflare/core/snippet-injector.js +96 -0
- package/dist/cloudflare/core/types/design-code.js +4 -0
- package/dist/cloudflare/core/types/enriched.js +5 -0
- package/dist/cloudflare/core/types/index.js +4 -0
- package/dist/cloudflare/core/websocket-connector.js +256 -0
- package/dist/cloudflare/core/websocket-server.js +646 -0
- package/dist/cloudflare/core/write-tools.js +2091 -0
- package/dist/cloudflare/index.js +2899 -0
- package/dist/cloudflare/test-browser.js +88 -0
- package/dist/core/comment-tools.d.ts +11 -0
- package/dist/core/comment-tools.d.ts.map +1 -0
- package/dist/core/comment-tools.js +293 -0
- package/dist/core/comment-tools.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +162 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/console-monitor.d.ts +82 -0
- package/dist/core/console-monitor.d.ts.map +1 -0
- package/dist/core/console-monitor.js +428 -0
- package/dist/core/console-monitor.js.map +1 -0
- package/dist/core/design-code-tools.d.ts +127 -0
- package/dist/core/design-code-tools.d.ts.map +1 -0
- package/dist/core/design-code-tools.js +2505 -0
- package/dist/core/design-code-tools.js.map +1 -0
- package/dist/core/design-system-manifest.d.ts +272 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -0
- package/dist/core/design-system-manifest.js +261 -0
- package/dist/core/design-system-manifest.js.map +1 -0
- package/dist/core/design-system-tools.d.ts +17 -0
- package/dist/core/design-system-tools.d.ts.map +1 -0
- package/dist/core/design-system-tools.js +864 -0
- package/dist/core/design-system-tools.js.map +1 -0
- package/dist/core/enrichment/enrichment-service.d.ts +52 -0
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
- package/dist/core/enrichment/enrichment-service.js +273 -0
- package/dist/core/enrichment/enrichment-service.js.map +1 -0
- package/dist/core/enrichment/index.d.ts +8 -0
- package/dist/core/enrichment/index.d.ts.map +1 -0
- package/dist/core/enrichment/index.js +8 -0
- package/dist/core/enrichment/index.js.map +1 -0
- package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
- package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
- package/dist/core/enrichment/relationship-mapper.js +352 -0
- package/dist/core/enrichment/relationship-mapper.js.map +1 -0
- package/dist/core/enrichment/style-resolver.d.ts +80 -0
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
- package/dist/core/enrichment/style-resolver.js +327 -0
- package/dist/core/enrichment/style-resolver.js.map +1 -0
- package/dist/core/figma-api.d.ts +201 -0
- package/dist/core/figma-api.d.ts.map +1 -0
- package/dist/core/figma-api.js +410 -0
- package/dist/core/figma-api.js.map +1 -0
- package/dist/core/figma-connector.d.ts +48 -0
- package/dist/core/figma-connector.d.ts.map +1 -0
- package/dist/core/figma-connector.js +8 -0
- package/dist/core/figma-connector.js.map +1 -0
- package/dist/core/figma-desktop-connector.d.ts +265 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -0
- package/dist/core/figma-desktop-connector.js +1184 -0
- package/dist/core/figma-desktop-connector.js.map +1 -0
- package/dist/core/figma-reconstruction-spec.d.ts +166 -0
- package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
- package/dist/core/figma-reconstruction-spec.js +403 -0
- package/dist/core/figma-reconstruction-spec.js.map +1 -0
- package/dist/core/figma-style-extractor.d.ts +76 -0
- package/dist/core/figma-style-extractor.d.ts.map +1 -0
- package/dist/core/figma-style-extractor.js +312 -0
- package/dist/core/figma-style-extractor.js.map +1 -0
- package/dist/core/figma-tools.d.ts +23 -0
- package/dist/core/figma-tools.d.ts.map +1 -0
- package/dist/core/figma-tools.js +2948 -0
- package/dist/core/figma-tools.js.map +1 -0
- package/dist/core/logger.d.ts +22 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/port-discovery.d.ts +110 -0
- package/dist/core/port-discovery.d.ts.map +1 -0
- package/dist/core/port-discovery.js +283 -0
- package/dist/core/port-discovery.js.map +1 -0
- package/dist/core/snippet-injector.d.ts +24 -0
- package/dist/core/snippet-injector.d.ts.map +1 -0
- package/dist/core/snippet-injector.js +97 -0
- package/dist/core/snippet-injector.js.map +1 -0
- package/dist/core/types/design-code.d.ts +262 -0
- package/dist/core/types/design-code.d.ts.map +1 -0
- package/dist/core/types/design-code.js +5 -0
- package/dist/core/types/design-code.js.map +1 -0
- package/dist/core/types/enriched.d.ts +213 -0
- package/dist/core/types/enriched.d.ts.map +1 -0
- package/dist/core/types/enriched.js +6 -0
- package/dist/core/types/enriched.js.map +1 -0
- package/dist/core/types/index.d.ts +112 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +5 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/core/websocket-connector.d.ts +55 -0
- package/dist/core/websocket-connector.d.ts.map +1 -0
- package/dist/core/websocket-connector.js +257 -0
- package/dist/core/websocket-connector.js.map +1 -0
- package/dist/core/websocket-server.d.ts +191 -0
- package/dist/core/websocket-server.d.ts.map +1 -0
- package/dist/core/websocket-server.js +647 -0
- package/dist/core/websocket-server.js.map +1 -0
- package/dist/core/write-tools.d.ts +7 -0
- package/dist/core/write-tools.d.ts.map +1 -0
- package/dist/core/write-tools.js +2092 -0
- package/dist/core/write-tools.js.map +1 -0
- package/dist/local.d.ts +84 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +5039 -0
- package/dist/local.js.map +1 -0
- package/figma-desktop-bridge/README.md +313 -0
- package/figma-desktop-bridge/code.js +2818 -0
- package/figma-desktop-bridge/manifest.json +67 -0
- package/figma-desktop-bridge/ui.html +1236 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma Desktop Connector
|
|
3
|
+
*
|
|
4
|
+
* This service connects directly to Figma Desktop's plugin context
|
|
5
|
+
* to execute code with access to the full Figma Plugin API,
|
|
6
|
+
* including variables without Enterprise access.
|
|
7
|
+
*
|
|
8
|
+
* Uses Puppeteer's Worker API to directly access plugin workers.
|
|
9
|
+
* Note: This is a legacy connector maintained for backwards compatibility.
|
|
10
|
+
* The WebSocket Desktop Bridge plugin is the primary connection method.
|
|
11
|
+
*/
|
|
12
|
+
import { logger } from './logger.js';
|
|
13
|
+
export class FigmaDesktopConnector {
|
|
14
|
+
constructor(page) {
|
|
15
|
+
this.cachedPluginFrame = null;
|
|
16
|
+
this.page = page;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Clear the cached plugin UI frame reference.
|
|
20
|
+
* Called automatically when a detached frame is detected.
|
|
21
|
+
*/
|
|
22
|
+
clearFrameCache() {
|
|
23
|
+
if (this.cachedPluginFrame) {
|
|
24
|
+
logger.debug('Clearing cached plugin UI frame reference');
|
|
25
|
+
this.cachedPluginFrame = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Conditionally log to Figma's browser console (only when DEBUG is enabled).
|
|
30
|
+
* Skips the CDP roundtrip entirely in production, eliminating ~5-15ms per call.
|
|
31
|
+
*/
|
|
32
|
+
async logToFigmaConsole(fn, ...args) {
|
|
33
|
+
if (!FigmaDesktopConnector.DEBUG)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
await this.page.evaluate(fn, ...args);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Ignore logging failures
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Initialize connection to Figma Desktop's plugin context
|
|
44
|
+
* No setup needed - Puppeteer handles worker access automatically
|
|
45
|
+
*/
|
|
46
|
+
async initialize() {
|
|
47
|
+
logger.info('Figma Desktop connector initialized (using Puppeteer Worker API)');
|
|
48
|
+
}
|
|
49
|
+
getTransportType() {
|
|
50
|
+
return 'cdp';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Execute code in Figma's plugin context where the figma API is available
|
|
54
|
+
* Uses Puppeteer's direct worker access instead of CDP context enumeration
|
|
55
|
+
*/
|
|
56
|
+
async executeInPluginContext(code) {
|
|
57
|
+
try {
|
|
58
|
+
// Use Puppeteer's worker API directly - this can access plugin workers
|
|
59
|
+
// that CDP's Runtime.getExecutionContexts cannot enumerate
|
|
60
|
+
const workers = this.page.workers();
|
|
61
|
+
// Log to browser console so MCP can capture it
|
|
62
|
+
await this.logToFigmaConsole((count, urls) => {
|
|
63
|
+
console.log(`[DESKTOP_CONNECTOR] Found ${count} workers via Puppeteer API:`, urls);
|
|
64
|
+
}, workers.length, workers.map(w => w.url()));
|
|
65
|
+
logger.info({
|
|
66
|
+
workerCount: workers.length,
|
|
67
|
+
workerUrls: workers.map(w => w.url())
|
|
68
|
+
}, 'Found workers via Puppeteer API');
|
|
69
|
+
// Try each worker to find one with figma API
|
|
70
|
+
for (const worker of workers) {
|
|
71
|
+
try {
|
|
72
|
+
// Log to browser console
|
|
73
|
+
await this.logToFigmaConsole((url) => {
|
|
74
|
+
console.log(`[DESKTOP_CONNECTOR] Checking worker: ${url}`);
|
|
75
|
+
}, worker.url());
|
|
76
|
+
// Check if this worker has the figma API
|
|
77
|
+
// Use string evaluation to avoid TypeScript errors about figma global
|
|
78
|
+
const hasFigmaApi = await worker.evaluate('typeof figma !== "undefined"');
|
|
79
|
+
// Log result to browser console
|
|
80
|
+
await this.logToFigmaConsole((url, hasApi) => {
|
|
81
|
+
console.log(`[DESKTOP_CONNECTOR] Worker ${url} has figma API: ${hasApi}`);
|
|
82
|
+
}, worker.url(), hasFigmaApi);
|
|
83
|
+
if (hasFigmaApi) {
|
|
84
|
+
logger.info({ workerUrl: worker.url() }, 'Found worker with Figma API');
|
|
85
|
+
await this.logToFigmaConsole((url) => {
|
|
86
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ SUCCESS! Found worker with Figma API: ${url}`);
|
|
87
|
+
}, worker.url());
|
|
88
|
+
// Execute the code in this worker context
|
|
89
|
+
// Wrap the code in a function to ensure proper evaluation
|
|
90
|
+
const wrappedCode = `(${code})`;
|
|
91
|
+
const result = await worker.evaluate(wrappedCode);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (workerError) {
|
|
96
|
+
// This worker doesn't have figma API or evaluation failed, try next
|
|
97
|
+
await this.logToFigmaConsole((url, err) => {
|
|
98
|
+
console.error(`[DESKTOP_CONNECTOR] ❌ Worker ${url} check failed:`, err);
|
|
99
|
+
}, worker.url(), workerError instanceof Error ? workerError.message : String(workerError));
|
|
100
|
+
logger.error({ error: workerError, workerUrl: worker.url() }, 'Worker check failed, trying next');
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// If no worker found with figma API, throw error
|
|
105
|
+
throw new Error('No plugin worker found with Figma API. Make sure a plugin is running in Figma Desktop.');
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.error({ error, code: code.substring(0, 200) }, 'Failed to execute in plugin context');
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get Figma variables from plugin UI window object
|
|
114
|
+
* This bypasses Figma's plugin sandbox security restrictions
|
|
115
|
+
* by accessing data that the plugin posted to its UI iframe
|
|
116
|
+
*/
|
|
117
|
+
async getVariablesFromPluginUI(fileKey) {
|
|
118
|
+
try {
|
|
119
|
+
// Log to browser console
|
|
120
|
+
await this.logToFigmaConsole((key) => {
|
|
121
|
+
console.log(`[DESKTOP_CONNECTOR] 🚀 getVariablesFromPluginUI() called, fileKey: ${key}`);
|
|
122
|
+
}, fileKey);
|
|
123
|
+
logger.info({ fileKey }, 'Getting variables from plugin UI iframe');
|
|
124
|
+
// Get all frames (iframes) in the page
|
|
125
|
+
const frames = this.page.frames();
|
|
126
|
+
await this.logToFigmaConsole((count) => {
|
|
127
|
+
console.log(`[DESKTOP_CONNECTOR] Found ${count} frames (iframes)`);
|
|
128
|
+
}, frames.length);
|
|
129
|
+
logger.info({ frameCount: frames.length }, 'Found frames in page');
|
|
130
|
+
// Try to find plugin UI iframe with variables data
|
|
131
|
+
for (const frame of frames) {
|
|
132
|
+
try {
|
|
133
|
+
// Check if frame is still attached before accessing it
|
|
134
|
+
if (frame.isDetached()) {
|
|
135
|
+
logger.debug('Skipping detached frame');
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const frameUrl = frame.url();
|
|
139
|
+
await this.logToFigmaConsole((url) => {
|
|
140
|
+
console.log(`[DESKTOP_CONNECTOR] Checking frame: ${url}`);
|
|
141
|
+
}, frameUrl);
|
|
142
|
+
// Check if this frame has our variables data
|
|
143
|
+
const hasData = await frame.evaluate('typeof window.__figmaVariablesData !== "undefined" && window.__figmaVariablesReady === true');
|
|
144
|
+
await this.logToFigmaConsole((url, has) => {
|
|
145
|
+
console.log(`[DESKTOP_CONNECTOR] Frame ${url} has variables data: ${has}`);
|
|
146
|
+
}, frameUrl, hasData);
|
|
147
|
+
if (hasData) {
|
|
148
|
+
logger.info({ frameUrl }, 'Found frame with variables data');
|
|
149
|
+
await this.logToFigmaConsole((url) => {
|
|
150
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ SUCCESS! Found plugin UI with variables data: ${url}`);
|
|
151
|
+
}, frameUrl);
|
|
152
|
+
// Get the data from window object
|
|
153
|
+
const result = await frame.evaluate('window.__figmaVariablesData');
|
|
154
|
+
logger.info({
|
|
155
|
+
variableCount: result.variables?.length,
|
|
156
|
+
collectionCount: result.variableCollections?.length
|
|
157
|
+
}, 'Successfully retrieved variables from plugin UI');
|
|
158
|
+
await this.logToFigmaConsole((varCount, collCount) => {
|
|
159
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Retrieved ${varCount} variables in ${collCount} collections`);
|
|
160
|
+
}, result.variables?.length || 0, result.variableCollections?.length || 0);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (frameError) {
|
|
165
|
+
const errorMsg = frameError instanceof Error ? frameError.message : String(frameError);
|
|
166
|
+
const isDetachedError = errorMsg.includes('detached') || errorMsg.includes('Execution context was destroyed');
|
|
167
|
+
// Safely get frame URL (may fail if frame is detached)
|
|
168
|
+
let safeFrameUrl = 'unknown';
|
|
169
|
+
try {
|
|
170
|
+
safeFrameUrl = frame.url();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
safeFrameUrl = '(detached)';
|
|
174
|
+
}
|
|
175
|
+
if (isDetachedError) {
|
|
176
|
+
logger.debug({ frameUrl: safeFrameUrl }, 'Frame was detached during variables check, trying next');
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
await this.logToFigmaConsole((url, err) => {
|
|
180
|
+
console.log(`[DESKTOP_CONNECTOR] Frame ${url} check failed: ${err}`);
|
|
181
|
+
}, safeFrameUrl, errorMsg);
|
|
182
|
+
logger.debug({ error: frameError, frameUrl: safeFrameUrl }, 'Frame check failed, trying next');
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// If no frame found with data, throw error
|
|
188
|
+
throw new Error('No plugin UI found with variables data. Make sure the Variables Exporter (Persistent) plugin is running.');
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
logger.error({ error }, 'Failed to get variables from plugin UI');
|
|
192
|
+
await this.logToFigmaConsole((msg) => {
|
|
193
|
+
console.error('[DESKTOP_CONNECTOR] ❌ getVariablesFromPluginUI failed:', msg);
|
|
194
|
+
}, error instanceof Error ? error.message : String(error));
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get component data by node ID from plugin UI window object
|
|
200
|
+
* This bypasses the REST API bug where descriptions are missing
|
|
201
|
+
* by accessing data from the Desktop Bridge plugin via its UI iframe
|
|
202
|
+
*/
|
|
203
|
+
async getComponentFromPluginUI(nodeId) {
|
|
204
|
+
try {
|
|
205
|
+
// Log to browser console
|
|
206
|
+
await this.logToFigmaConsole((id) => {
|
|
207
|
+
console.log(`[DESKTOP_CONNECTOR] 🎯 getComponentFromPluginUI() called, nodeId: ${id}`);
|
|
208
|
+
}, nodeId);
|
|
209
|
+
logger.info({ nodeId }, 'Getting component from plugin UI iframe');
|
|
210
|
+
// Get all frames (iframes) in the page
|
|
211
|
+
const frames = this.page.frames();
|
|
212
|
+
await this.logToFigmaConsole((count) => {
|
|
213
|
+
console.log(`[DESKTOP_CONNECTOR] Found ${count} frames (iframes)`);
|
|
214
|
+
}, frames.length);
|
|
215
|
+
logger.info({ frameCount: frames.length }, 'Found frames in page');
|
|
216
|
+
// Try to find plugin UI iframe with requestComponentData function
|
|
217
|
+
for (const frame of frames) {
|
|
218
|
+
try {
|
|
219
|
+
// Check if frame is still attached before accessing it
|
|
220
|
+
if (frame.isDetached()) {
|
|
221
|
+
logger.debug('Skipping detached frame');
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const frameUrl = frame.url();
|
|
225
|
+
await this.logToFigmaConsole((url) => {
|
|
226
|
+
console.log(`[DESKTOP_CONNECTOR] Checking frame: ${url}`);
|
|
227
|
+
}, frameUrl);
|
|
228
|
+
// Check if this frame has our requestComponentData function
|
|
229
|
+
const hasFunction = await frame.evaluate('typeof window.requestComponentData === "function"');
|
|
230
|
+
await this.logToFigmaConsole((url, has) => {
|
|
231
|
+
console.log(`[DESKTOP_CONNECTOR] Frame ${url} has requestComponentData: ${has}`);
|
|
232
|
+
}, frameUrl, hasFunction);
|
|
233
|
+
if (hasFunction) {
|
|
234
|
+
logger.info({ frameUrl }, 'Found frame with requestComponentData function');
|
|
235
|
+
await this.logToFigmaConsole((url) => {
|
|
236
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ SUCCESS! Found plugin UI with requestComponentData: ${url}`);
|
|
237
|
+
}, frameUrl);
|
|
238
|
+
// Call the function with the nodeId - it returns a Promise
|
|
239
|
+
// Use JSON.stringify to safely pass the nodeId as a string literal
|
|
240
|
+
const result = await frame.evaluate(`window.requestComponentData(${JSON.stringify(nodeId)})`);
|
|
241
|
+
logger.info({
|
|
242
|
+
nodeId,
|
|
243
|
+
componentName: result.component?.name,
|
|
244
|
+
hasDescription: !!result.component?.description
|
|
245
|
+
}, 'Successfully retrieved component from plugin UI');
|
|
246
|
+
await this.logToFigmaConsole((name, hasDesc) => {
|
|
247
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Retrieved component "${name}", has description: ${hasDesc}`);
|
|
248
|
+
}, result.component?.name, !!result.component?.description);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (frameError) {
|
|
253
|
+
const errorMsg = frameError instanceof Error ? frameError.message : String(frameError);
|
|
254
|
+
const isDetachedError = errorMsg.includes('detached') || errorMsg.includes('Execution context was destroyed');
|
|
255
|
+
// Safely get frame URL (may fail if frame is detached)
|
|
256
|
+
let safeFrameUrl = 'unknown';
|
|
257
|
+
try {
|
|
258
|
+
safeFrameUrl = frame.url();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
safeFrameUrl = '(detached)';
|
|
262
|
+
}
|
|
263
|
+
if (isDetachedError) {
|
|
264
|
+
logger.debug({ frameUrl: safeFrameUrl }, 'Frame was detached during component check, trying next');
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
await this.logToFigmaConsole((url, err) => {
|
|
268
|
+
console.log(`[DESKTOP_CONNECTOR] Frame ${url} check failed: ${err}`);
|
|
269
|
+
}, safeFrameUrl, errorMsg);
|
|
270
|
+
logger.debug({ error: frameError, frameUrl: safeFrameUrl }, 'Frame check failed, trying next');
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// If no frame found with function, throw error
|
|
276
|
+
throw new Error('No plugin UI found with requestComponentData function. Make sure the Desktop Bridge plugin is running.');
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
logger.error({ error, nodeId }, 'Failed to get component from plugin UI');
|
|
280
|
+
await this.logToFigmaConsole((msg) => {
|
|
281
|
+
console.error('[DESKTOP_CONNECTOR] ❌ getComponentFromPluginUI failed:', msg);
|
|
282
|
+
}, error instanceof Error ? error.message : String(error));
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get Figma variables using the desktop connection
|
|
288
|
+
* This bypasses the Enterprise requirement!
|
|
289
|
+
*/
|
|
290
|
+
async getVariables(fileKey) {
|
|
291
|
+
// Log to browser console
|
|
292
|
+
await this.logToFigmaConsole((key) => {
|
|
293
|
+
console.log(`[DESKTOP_CONNECTOR] 🚀 getVariables() called, fileKey: ${key}`);
|
|
294
|
+
}, fileKey);
|
|
295
|
+
logger.info({ fileKey }, 'Getting variables via Desktop connection');
|
|
296
|
+
const code = `
|
|
297
|
+
(async () => {
|
|
298
|
+
try {
|
|
299
|
+
// Check if we're in the right context
|
|
300
|
+
if (typeof figma === 'undefined') {
|
|
301
|
+
throw new Error('Figma API not available in this context');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Get variables just like the official MCP does
|
|
305
|
+
const variables = await figma.variables.getLocalVariablesAsync();
|
|
306
|
+
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
307
|
+
|
|
308
|
+
// Format the response with file metadata for context verification
|
|
309
|
+
const result = {
|
|
310
|
+
success: true,
|
|
311
|
+
timestamp: Date.now(),
|
|
312
|
+
// Include file metadata so we can verify we're querying the right file
|
|
313
|
+
fileMetadata: {
|
|
314
|
+
fileName: figma.root.name,
|
|
315
|
+
fileKey: figma.fileKey || null
|
|
316
|
+
},
|
|
317
|
+
variables: variables.map(v => ({
|
|
318
|
+
id: v.id,
|
|
319
|
+
name: v.name,
|
|
320
|
+
key: v.key,
|
|
321
|
+
resolvedType: v.resolvedType,
|
|
322
|
+
valuesByMode: v.valuesByMode,
|
|
323
|
+
variableCollectionId: v.variableCollectionId,
|
|
324
|
+
scopes: v.scopes,
|
|
325
|
+
description: v.description,
|
|
326
|
+
hiddenFromPublishing: v.hiddenFromPublishing
|
|
327
|
+
})),
|
|
328
|
+
variableCollections: collections.map(c => ({
|
|
329
|
+
id: c.id,
|
|
330
|
+
name: c.name,
|
|
331
|
+
key: c.key,
|
|
332
|
+
modes: c.modes,
|
|
333
|
+
defaultModeId: c.defaultModeId,
|
|
334
|
+
variableIds: c.variableIds
|
|
335
|
+
}))
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return result;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
error: error.message
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
})()
|
|
346
|
+
`;
|
|
347
|
+
try {
|
|
348
|
+
const result = await this.executeInPluginContext(code);
|
|
349
|
+
if (!result.success) {
|
|
350
|
+
throw new Error(result.error || 'Failed to get variables');
|
|
351
|
+
}
|
|
352
|
+
logger.info({
|
|
353
|
+
variableCount: result.variables?.length,
|
|
354
|
+
collectionCount: result.variableCollections?.length
|
|
355
|
+
}, 'Successfully retrieved variables via Desktop');
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
logger.error({ error }, 'Failed to get variables via Desktop');
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Clean up resources (no-op since we use Puppeteer's built-in worker management)
|
|
365
|
+
*/
|
|
366
|
+
/**
|
|
367
|
+
* Get component data by node ID using Plugin API
|
|
368
|
+
* This bypasses the REST API bug where descriptions are missing
|
|
369
|
+
*/
|
|
370
|
+
async getComponentByNodeId(nodeId) {
|
|
371
|
+
await this.logToFigmaConsole((id) => {
|
|
372
|
+
console.log(`[DESKTOP_CONNECTOR] 🎯 getComponentByNodeId() called, nodeId: ${id}`);
|
|
373
|
+
}, nodeId);
|
|
374
|
+
logger.info({ nodeId }, 'Getting component via Desktop Plugin API');
|
|
375
|
+
const code = `
|
|
376
|
+
(async () => {
|
|
377
|
+
try {
|
|
378
|
+
// Check if we're in the right context
|
|
379
|
+
if (typeof figma === 'undefined') {
|
|
380
|
+
throw new Error('Figma API not available in this context');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Get the node by ID
|
|
384
|
+
const node = figma.getNodeById('${nodeId}');
|
|
385
|
+
|
|
386
|
+
if (!node) {
|
|
387
|
+
throw new Error('Node not found with ID: ${nodeId}');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Check if it's a component-like node
|
|
391
|
+
if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'INSTANCE') {
|
|
392
|
+
throw new Error('Node is not a component, component set, or instance. Type: ' + node.type);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Detect if this is a variant (COMPONENT inside a COMPONENT_SET)
|
|
396
|
+
// Note: Can't use optional chaining (?.) - Figma plugin sandbox doesn't support it
|
|
397
|
+
const isVariant = node.type === 'COMPONENT' && node.parent && node.parent.type === 'COMPONENT_SET';
|
|
398
|
+
|
|
399
|
+
// Extract component data including description fields
|
|
400
|
+
const result = {
|
|
401
|
+
success: true,
|
|
402
|
+
timestamp: Date.now(),
|
|
403
|
+
component: {
|
|
404
|
+
id: node.id,
|
|
405
|
+
name: node.name,
|
|
406
|
+
type: node.type,
|
|
407
|
+
// Variants CAN have their own description
|
|
408
|
+
description: node.description || null,
|
|
409
|
+
descriptionMarkdown: node.descriptionMarkdown || null,
|
|
410
|
+
// Include other useful properties
|
|
411
|
+
visible: node.visible,
|
|
412
|
+
locked: node.locked,
|
|
413
|
+
// Flag to indicate if this is a variant
|
|
414
|
+
isVariant: isVariant,
|
|
415
|
+
// For component sets and non-variant components only (variants cannot access this)
|
|
416
|
+
componentPropertyDefinitions: node.type === 'COMPONENT_SET' || (node.type === 'COMPONENT' && !isVariant)
|
|
417
|
+
? node.componentPropertyDefinitions
|
|
418
|
+
: undefined,
|
|
419
|
+
// Get children info (lightweight)
|
|
420
|
+
children: node.children ? node.children.map(child => ({
|
|
421
|
+
id: child.id,
|
|
422
|
+
name: child.name,
|
|
423
|
+
type: child.type
|
|
424
|
+
})) : undefined
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
return result;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
return {
|
|
431
|
+
success: false,
|
|
432
|
+
error: error.message
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
})()
|
|
436
|
+
`;
|
|
437
|
+
try {
|
|
438
|
+
const result = await this.executeInPluginContext(code);
|
|
439
|
+
if (!result.success) {
|
|
440
|
+
throw new Error(result.error || 'Failed to get component data');
|
|
441
|
+
}
|
|
442
|
+
logger.info({
|
|
443
|
+
nodeId,
|
|
444
|
+
componentName: result.component?.name,
|
|
445
|
+
hasDescription: !!result.component?.description
|
|
446
|
+
}, 'Successfully retrieved component via Desktop Plugin API');
|
|
447
|
+
await this.logToFigmaConsole((name, hasDesc) => {
|
|
448
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Retrieved component "${name}", has description: ${hasDesc}`);
|
|
449
|
+
}, result.component?.name, !!result.component?.description);
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
logger.error({ error, nodeId }, 'Failed to get component via Desktop Plugin API');
|
|
454
|
+
await this.logToFigmaConsole((id, err) => {
|
|
455
|
+
console.error(`[DESKTOP_CONNECTOR] ❌ getComponentByNodeId failed for ${id}:`, err);
|
|
456
|
+
}, nodeId, error instanceof Error ? error.message : String(error));
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async dispose() {
|
|
461
|
+
logger.info('Figma Desktop connector disposed');
|
|
462
|
+
}
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// WRITE OPERATIONS - Execute commands via Plugin UI iframe
|
|
465
|
+
// ============================================================================
|
|
466
|
+
/**
|
|
467
|
+
* Find the Desktop Bridge plugin UI iframe
|
|
468
|
+
* Returns the frame that has the write operation functions
|
|
469
|
+
* Handles detached frame errors gracefully
|
|
470
|
+
*/
|
|
471
|
+
async findPluginUIFrame() {
|
|
472
|
+
// Return cached frame if still valid
|
|
473
|
+
if (this.cachedPluginFrame) {
|
|
474
|
+
try {
|
|
475
|
+
if (!this.cachedPluginFrame.isDetached()) {
|
|
476
|
+
return this.cachedPluginFrame;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Frame reference is stale
|
|
481
|
+
}
|
|
482
|
+
this.cachedPluginFrame = null;
|
|
483
|
+
logger.debug('Cached plugin frame was detached, rescanning');
|
|
484
|
+
}
|
|
485
|
+
const frames = this.page.frames();
|
|
486
|
+
logger.debug({ frameCount: frames.length }, 'Searching for Desktop Bridge plugin UI frame');
|
|
487
|
+
for (const frame of frames) {
|
|
488
|
+
try {
|
|
489
|
+
// Skip detached frames
|
|
490
|
+
if (frame.isDetached()) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
// Check if this frame has the executeCode function (our Desktop Bridge plugin)
|
|
494
|
+
const hasWriteOps = await frame.evaluate('typeof window.executeCode === "function"');
|
|
495
|
+
if (hasWriteOps) {
|
|
496
|
+
logger.info({ frameUrl: frame.url() }, 'Found Desktop Bridge plugin UI frame');
|
|
497
|
+
this.cachedPluginFrame = frame;
|
|
498
|
+
return frame;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
// Frame might be inaccessible or detached, continue to next
|
|
503
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
504
|
+
if (errorMsg.includes('detached')) {
|
|
505
|
+
logger.debug({ frameUrl: frame.url() }, 'Frame was detached, skipping');
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
throw new Error('Desktop Bridge plugin UI not found. Make sure the Desktop Bridge plugin is running in Figma. ' +
|
|
511
|
+
'The plugin must be open for write operations to work.');
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Execute arbitrary code in Figma's plugin context
|
|
515
|
+
* This is the power tool that can run any Figma Plugin API code
|
|
516
|
+
* Includes retry logic for detached frame errors
|
|
517
|
+
*/
|
|
518
|
+
async executeCodeViaUI(code, timeout = 5000) {
|
|
519
|
+
await this.logToFigmaConsole((codeStr, timeoutMs) => {
|
|
520
|
+
console.log(`[DESKTOP_CONNECTOR] executeCodeViaUI() called, code length: ${codeStr.length}, timeout: ${timeoutMs}ms`);
|
|
521
|
+
}, code, timeout);
|
|
522
|
+
logger.info({ codeLength: code.length, timeout }, 'Executing code via plugin UI');
|
|
523
|
+
// Retry logic for detached frame errors
|
|
524
|
+
const maxRetries = 2;
|
|
525
|
+
let lastError = null;
|
|
526
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
527
|
+
try {
|
|
528
|
+
const frame = await this.findPluginUIFrame();
|
|
529
|
+
// Check if frame is still valid before using it
|
|
530
|
+
if (frame.isDetached()) {
|
|
531
|
+
throw new Error('Frame became detached');
|
|
532
|
+
}
|
|
533
|
+
// Call the executeCode function in the UI iframe
|
|
534
|
+
const result = await frame.evaluate(`window.executeCode(${JSON.stringify(code)}, ${timeout})`);
|
|
535
|
+
logger.info({ success: result.success, error: result.error }, 'Code execution completed');
|
|
536
|
+
await this.logToFigmaConsole((success, errorMsg) => {
|
|
537
|
+
if (success) {
|
|
538
|
+
console.log('[DESKTOP_CONNECTOR] ✅ Code execution succeeded');
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
console.log('[DESKTOP_CONNECTOR] ⚠️ Code execution returned error:', errorMsg);
|
|
542
|
+
}
|
|
543
|
+
}, result.success, result.error || '');
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
548
|
+
lastError = error instanceof Error ? error : new Error(errorMsg);
|
|
549
|
+
// Check if it's a detached frame error
|
|
550
|
+
if (errorMsg.includes('detached') && attempt < maxRetries) {
|
|
551
|
+
logger.warn({ attempt, maxRetries }, 'Frame detached, retrying with fresh frames');
|
|
552
|
+
this.clearFrameCache();
|
|
553
|
+
// Small delay before retry
|
|
554
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
// Not a detached frame error or we've exhausted retries
|
|
558
|
+
logger.error({ error: errorMsg, attempt }, 'Code execution failed');
|
|
559
|
+
await this.logToFigmaConsole((err) => {
|
|
560
|
+
console.error('[DESKTOP_CONNECTOR] ❌ executeCodeViaUI failed:', err);
|
|
561
|
+
}, errorMsg);
|
|
562
|
+
throw lastError;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
throw lastError || new Error('Execution failed after retries');
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Update a variable's value in a specific mode
|
|
569
|
+
*/
|
|
570
|
+
async updateVariable(variableId, modeId, value) {
|
|
571
|
+
await this.logToFigmaConsole((vId, mId) => {
|
|
572
|
+
console.log(`[DESKTOP_CONNECTOR] updateVariable() called: ${vId} mode ${mId}`);
|
|
573
|
+
}, variableId, modeId);
|
|
574
|
+
logger.info({ variableId, modeId }, 'Updating variable via plugin UI');
|
|
575
|
+
const frame = await this.findPluginUIFrame();
|
|
576
|
+
try {
|
|
577
|
+
const result = await frame.evaluate(`window.updateVariable(${JSON.stringify(variableId)}, ${JSON.stringify(modeId)}, ${JSON.stringify(value)})`);
|
|
578
|
+
logger.info({ success: result.success, variableName: result.variable?.name }, 'Variable updated');
|
|
579
|
+
await this.logToFigmaConsole((name) => {
|
|
580
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Variable "${name}" updated successfully`);
|
|
581
|
+
}, result.variable?.name || variableId);
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
logger.error({ error, variableId }, 'Update variable failed');
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Create a new variable in a collection
|
|
591
|
+
*/
|
|
592
|
+
async createVariable(name, collectionId, resolvedType, options) {
|
|
593
|
+
await this.logToFigmaConsole((n, cId, type) => {
|
|
594
|
+
console.log(`[DESKTOP_CONNECTOR] createVariable() called: "${n}" in collection ${cId}, type: ${type}`);
|
|
595
|
+
}, name, collectionId, resolvedType);
|
|
596
|
+
logger.info({ name, collectionId, resolvedType }, 'Creating variable via plugin UI');
|
|
597
|
+
const frame = await this.findPluginUIFrame();
|
|
598
|
+
try {
|
|
599
|
+
const result = await frame.evaluate(`window.createVariable(${JSON.stringify(name)}, ${JSON.stringify(collectionId)}, ${JSON.stringify(resolvedType)}, ${JSON.stringify(options || {})})`);
|
|
600
|
+
logger.info({ success: result.success, variableId: result.variable?.id }, 'Variable created');
|
|
601
|
+
await this.logToFigmaConsole((id, n) => {
|
|
602
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Variable "${n}" created with ID: ${id}`);
|
|
603
|
+
}, result.variable?.id || 'unknown', name);
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
logger.error({ error, name }, 'Create variable failed');
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Create a new variable collection
|
|
613
|
+
*/
|
|
614
|
+
async createVariableCollection(name, options) {
|
|
615
|
+
await this.logToFigmaConsole((n) => {
|
|
616
|
+
console.log(`[DESKTOP_CONNECTOR] createVariableCollection() called: "${n}"`);
|
|
617
|
+
}, name);
|
|
618
|
+
logger.info({ name, options }, 'Creating variable collection via plugin UI');
|
|
619
|
+
const frame = await this.findPluginUIFrame();
|
|
620
|
+
try {
|
|
621
|
+
const result = await frame.evaluate(`window.createVariableCollection(${JSON.stringify(name)}, ${JSON.stringify(options || {})})`);
|
|
622
|
+
logger.info({ success: result.success, collectionId: result.collection?.id }, 'Collection created');
|
|
623
|
+
await this.logToFigmaConsole((id, n) => {
|
|
624
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Collection "${n}" created with ID: ${id}`);
|
|
625
|
+
}, result.collection?.id || 'unknown', name);
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
logger.error({ error, name }, 'Create collection failed');
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Delete a variable
|
|
635
|
+
*/
|
|
636
|
+
async deleteVariable(variableId) {
|
|
637
|
+
await this.logToFigmaConsole((vId) => {
|
|
638
|
+
console.log(`[DESKTOP_CONNECTOR] deleteVariable() called: ${vId}`);
|
|
639
|
+
}, variableId);
|
|
640
|
+
logger.info({ variableId }, 'Deleting variable via plugin UI');
|
|
641
|
+
const frame = await this.findPluginUIFrame();
|
|
642
|
+
try {
|
|
643
|
+
const result = await frame.evaluate(`window.deleteVariable(${JSON.stringify(variableId)})`);
|
|
644
|
+
logger.info({ success: result.success, deletedName: result.deleted?.name }, 'Variable deleted');
|
|
645
|
+
await this.logToFigmaConsole((name) => {
|
|
646
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Variable "${name}" deleted`);
|
|
647
|
+
}, result.deleted?.name || variableId);
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
logger.error({ error, variableId }, 'Delete variable failed');
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Delete a variable collection
|
|
657
|
+
*/
|
|
658
|
+
async deleteVariableCollection(collectionId) {
|
|
659
|
+
await this.logToFigmaConsole((cId) => {
|
|
660
|
+
console.log(`[DESKTOP_CONNECTOR] deleteVariableCollection() called: ${cId}`);
|
|
661
|
+
}, collectionId);
|
|
662
|
+
logger.info({ collectionId }, 'Deleting collection via plugin UI');
|
|
663
|
+
const frame = await this.findPluginUIFrame();
|
|
664
|
+
try {
|
|
665
|
+
const result = await frame.evaluate(`window.deleteVariableCollection(${JSON.stringify(collectionId)})`);
|
|
666
|
+
logger.info({ success: result.success, deletedName: result.deleted?.name }, 'Collection deleted');
|
|
667
|
+
await this.logToFigmaConsole((name, count) => {
|
|
668
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Collection "${name}" deleted (had ${count} variables)`);
|
|
669
|
+
}, result.deleted?.name || collectionId, result.deleted?.variableCount || 0);
|
|
670
|
+
return result;
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
logger.error({ error, collectionId }, 'Delete collection failed');
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Refresh variables data from Figma
|
|
679
|
+
*/
|
|
680
|
+
async refreshVariables() {
|
|
681
|
+
await this.logToFigmaConsole(() => {
|
|
682
|
+
console.log('[DESKTOP_CONNECTOR] refreshVariables() called');
|
|
683
|
+
});
|
|
684
|
+
logger.info('Refreshing variables via plugin UI');
|
|
685
|
+
const frame = await this.findPluginUIFrame();
|
|
686
|
+
try {
|
|
687
|
+
const result = await frame.evaluate('window.refreshVariables()');
|
|
688
|
+
logger.info({
|
|
689
|
+
success: result.success,
|
|
690
|
+
variableCount: result.data?.variables?.length,
|
|
691
|
+
collectionCount: result.data?.variableCollections?.length
|
|
692
|
+
}, 'Variables refreshed');
|
|
693
|
+
await this.logToFigmaConsole((vCount, cCount) => {
|
|
694
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Variables refreshed: ${vCount} variables in ${cCount} collections`);
|
|
695
|
+
}, result.data?.variables?.length || 0, result.data?.variableCollections?.length || 0);
|
|
696
|
+
return result;
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
logger.error({ error }, 'Refresh variables failed');
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Rename a variable
|
|
705
|
+
*/
|
|
706
|
+
async renameVariable(variableId, newName) {
|
|
707
|
+
await this.logToFigmaConsole((vId, name) => {
|
|
708
|
+
console.log(`[DESKTOP_CONNECTOR] renameVariable() called: ${vId} -> "${name}"`);
|
|
709
|
+
}, variableId, newName);
|
|
710
|
+
logger.info({ variableId, newName }, 'Renaming variable via plugin UI');
|
|
711
|
+
const frame = await this.findPluginUIFrame();
|
|
712
|
+
try {
|
|
713
|
+
// Look up old name from cached variables data, then rename — single CDP roundtrip
|
|
714
|
+
const result = await frame.evaluate(`(async () => {
|
|
715
|
+
var oldName;
|
|
716
|
+
var vars = window.__figmaVariablesData;
|
|
717
|
+
if (vars && vars.variables) {
|
|
718
|
+
var v = vars.variables.find(function(v) { return v.id === ${JSON.stringify(variableId)}; });
|
|
719
|
+
if (v) oldName = v.name;
|
|
720
|
+
}
|
|
721
|
+
var result = await window.renameVariable(${JSON.stringify(variableId)}, ${JSON.stringify(newName)});
|
|
722
|
+
if (oldName) result.oldName = oldName;
|
|
723
|
+
return result;
|
|
724
|
+
})()`);
|
|
725
|
+
logger.info({ success: result.success, oldName: result.oldName, newName: result.variable?.name }, 'Variable renamed');
|
|
726
|
+
await this.logToFigmaConsole((oldN, newN) => {
|
|
727
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Variable renamed from "${oldN}" to "${newN}"`);
|
|
728
|
+
}, result.oldName || 'unknown', result.variable?.name || newName);
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
logger.error({ error, variableId }, 'Rename variable failed');
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Set the description on a variable (not a node — variables use a separate API)
|
|
738
|
+
*/
|
|
739
|
+
async setVariableDescription(variableId, description) {
|
|
740
|
+
await this.logToFigmaConsole((vId, desc) => {
|
|
741
|
+
console.log(`[DESKTOP_CONNECTOR] setVariableDescription() called: ${vId} -> "${desc}"`);
|
|
742
|
+
}, variableId, description);
|
|
743
|
+
logger.info({ variableId, description }, 'Setting variable description via plugin UI');
|
|
744
|
+
const frame = await this.findPluginUIFrame();
|
|
745
|
+
try {
|
|
746
|
+
const result = await frame.evaluate(`window.setVariableDescription(${JSON.stringify(variableId)}, ${JSON.stringify(description)})`);
|
|
747
|
+
logger.info({ success: result.success, variableId }, 'Variable description set');
|
|
748
|
+
await this.logToFigmaConsole((vId, success) => {
|
|
749
|
+
console.log(`[DESKTOP_CONNECTOR] ${success ? '✅' : '❌'} Variable description ${success ? 'set' : 'failed'} for ${vId}`);
|
|
750
|
+
}, variableId, result.success);
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
logger.error({ error, variableId }, 'Set variable description failed');
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Add a mode to a variable collection
|
|
760
|
+
*/
|
|
761
|
+
async addMode(collectionId, modeName) {
|
|
762
|
+
await this.logToFigmaConsole((cId, name) => {
|
|
763
|
+
console.log(`[DESKTOP_CONNECTOR] addMode() called: "${name}" to collection ${cId}`);
|
|
764
|
+
}, collectionId, modeName);
|
|
765
|
+
logger.info({ collectionId, modeName }, 'Adding mode via plugin UI');
|
|
766
|
+
const frame = await this.findPluginUIFrame();
|
|
767
|
+
try {
|
|
768
|
+
const result = await frame.evaluate(`window.addMode(${JSON.stringify(collectionId)}, ${JSON.stringify(modeName)})`);
|
|
769
|
+
logger.info({ success: result.success, newModeId: result.newMode?.modeId }, 'Mode added');
|
|
770
|
+
await this.logToFigmaConsole((name, modeId) => {
|
|
771
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Mode "${name}" added with ID: ${modeId}`);
|
|
772
|
+
}, modeName, result.newMode?.modeId || 'unknown');
|
|
773
|
+
return result;
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
logger.error({ error, collectionId }, 'Add mode failed');
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Rename a mode in a variable collection
|
|
782
|
+
*/
|
|
783
|
+
async renameMode(collectionId, modeId, newName) {
|
|
784
|
+
await this.logToFigmaConsole((cId, mId, name) => {
|
|
785
|
+
console.log(`[DESKTOP_CONNECTOR] renameMode() called: mode ${mId} in collection ${cId} -> "${name}"`);
|
|
786
|
+
}, collectionId, modeId, newName);
|
|
787
|
+
logger.info({ collectionId, modeId, newName }, 'Renaming mode via plugin UI');
|
|
788
|
+
const frame = await this.findPluginUIFrame();
|
|
789
|
+
try {
|
|
790
|
+
// Look up old mode name from cached variables data, then rename — single CDP roundtrip
|
|
791
|
+
const result = await frame.evaluate(`(async () => {
|
|
792
|
+
var oldName;
|
|
793
|
+
var vars = window.__figmaVariablesData;
|
|
794
|
+
var colls = vars && (vars.variableCollections || vars.collections);
|
|
795
|
+
if (colls) {
|
|
796
|
+
for (var i = 0; i < colls.length; i++) {
|
|
797
|
+
var c = colls[i];
|
|
798
|
+
if (c.id === ${JSON.stringify(collectionId)}) {
|
|
799
|
+
var mode = c.modes.find(function(m) { return m.modeId === ${JSON.stringify(modeId)}; });
|
|
800
|
+
if (mode) oldName = mode.name;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
var result = await window.renameMode(${JSON.stringify(collectionId)}, ${JSON.stringify(modeId)}, ${JSON.stringify(newName)});
|
|
806
|
+
if (oldName) result.oldName = oldName;
|
|
807
|
+
return result;
|
|
808
|
+
})()`);
|
|
809
|
+
logger.info({ success: result.success, oldName: result.oldName, newName }, 'Mode renamed');
|
|
810
|
+
await this.logToFigmaConsole((oldN, newN) => {
|
|
811
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Mode renamed from "${oldN}" to "${newN}"`);
|
|
812
|
+
}, result.oldName || 'unknown', newName);
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
logger.error({ error, collectionId, modeId }, 'Rename mode failed');
|
|
817
|
+
throw error;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Get all local components for design system manifest generation
|
|
822
|
+
*/
|
|
823
|
+
async getLocalComponents() {
|
|
824
|
+
await this.logToFigmaConsole(() => {
|
|
825
|
+
console.log('[DESKTOP_CONNECTOR] getLocalComponents() called');
|
|
826
|
+
});
|
|
827
|
+
logger.info('Getting local components via plugin UI');
|
|
828
|
+
const frame = await this.findPluginUIFrame();
|
|
829
|
+
try {
|
|
830
|
+
const result = await frame.evaluate('window.getLocalComponents()');
|
|
831
|
+
logger.info({
|
|
832
|
+
success: result.success,
|
|
833
|
+
componentCount: result.data?.totalComponents,
|
|
834
|
+
componentSetCount: result.data?.totalComponentSets
|
|
835
|
+
}, 'Local components retrieved');
|
|
836
|
+
await this.logToFigmaConsole((cCount, csCount) => {
|
|
837
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Found ${cCount} components and ${csCount} component sets`);
|
|
838
|
+
}, result.data?.totalComponents || 0, result.data?.totalComponentSets || 0);
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
logger.error({ error }, 'Get local components failed');
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Instantiate a component with overrides
|
|
848
|
+
* Supports both published library components (by key) and local components (by nodeId)
|
|
849
|
+
*/
|
|
850
|
+
async instantiateComponent(componentKey, options) {
|
|
851
|
+
await this.logToFigmaConsole((key, nodeId) => {
|
|
852
|
+
console.log(`[DESKTOP_CONNECTOR] instantiateComponent() called: key=${key}, nodeId=${nodeId}`);
|
|
853
|
+
}, componentKey, options?.nodeId || null);
|
|
854
|
+
logger.info({ componentKey, nodeId: options?.nodeId, options }, 'Instantiating component via plugin UI');
|
|
855
|
+
const frame = await this.findPluginUIFrame();
|
|
856
|
+
try {
|
|
857
|
+
const result = await frame.evaluate(`window.instantiateComponent(${JSON.stringify(componentKey)}, ${JSON.stringify(options || {})})`);
|
|
858
|
+
logger.info({ success: result.success, instanceId: result.instance?.id }, 'Component instantiated');
|
|
859
|
+
await this.logToFigmaConsole((instanceId, name) => {
|
|
860
|
+
console.log(`[DESKTOP_CONNECTOR] ✅ Component instantiated: ${name} (${instanceId})`);
|
|
861
|
+
}, result.instance?.id || 'unknown', result.instance?.name || 'unknown');
|
|
862
|
+
return result;
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
logger.error({ error, componentKey }, 'Instantiate component failed');
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
// ============================================================================
|
|
870
|
+
// NEW: COMPONENT PROPERTY MANAGEMENT
|
|
871
|
+
// ============================================================================
|
|
872
|
+
/**
|
|
873
|
+
* Set description on a component or style
|
|
874
|
+
*/
|
|
875
|
+
async setNodeDescription(nodeId, description, descriptionMarkdown) {
|
|
876
|
+
logger.info({ nodeId, descriptionLength: description.length }, 'Setting node description via plugin UI');
|
|
877
|
+
const frame = await this.findPluginUIFrame();
|
|
878
|
+
try {
|
|
879
|
+
const result = await frame.evaluate(`window.setNodeDescription(${JSON.stringify(nodeId)}, ${JSON.stringify(description)}, ${JSON.stringify(descriptionMarkdown)})`);
|
|
880
|
+
logger.info({ success: result.success, nodeName: result.node?.name }, 'Description set');
|
|
881
|
+
return result;
|
|
882
|
+
}
|
|
883
|
+
catch (error) {
|
|
884
|
+
logger.error({ error, nodeId }, 'Set description failed');
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Add a component property
|
|
890
|
+
*/
|
|
891
|
+
async addComponentProperty(nodeId, propertyName, type, defaultValue, options) {
|
|
892
|
+
logger.info({ nodeId, propertyName, type }, 'Adding component property via plugin UI');
|
|
893
|
+
const frame = await this.findPluginUIFrame();
|
|
894
|
+
try {
|
|
895
|
+
const result = await frame.evaluate(`window.addComponentProperty(${JSON.stringify(nodeId)}, ${JSON.stringify(propertyName)}, ${JSON.stringify(type)}, ${JSON.stringify(defaultValue)}, ${JSON.stringify(options || {})})`);
|
|
896
|
+
logger.info({ success: result.success, propertyName: result.propertyName }, 'Property added');
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
logger.error({ error, nodeId, propertyName }, 'Add component property failed');
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Edit an existing component property
|
|
906
|
+
*/
|
|
907
|
+
async editComponentProperty(nodeId, propertyName, newValue) {
|
|
908
|
+
logger.info({ nodeId, propertyName }, 'Editing component property via plugin UI');
|
|
909
|
+
const frame = await this.findPluginUIFrame();
|
|
910
|
+
try {
|
|
911
|
+
const result = await frame.evaluate(`window.editComponentProperty(${JSON.stringify(nodeId)}, ${JSON.stringify(propertyName)}, ${JSON.stringify(newValue)})`);
|
|
912
|
+
logger.info({ success: result.success, propertyName: result.propertyName }, 'Property edited');
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
logger.error({ error, nodeId, propertyName }, 'Edit component property failed');
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Delete a component property
|
|
922
|
+
*/
|
|
923
|
+
async deleteComponentProperty(nodeId, propertyName) {
|
|
924
|
+
logger.info({ nodeId, propertyName }, 'Deleting component property via plugin UI');
|
|
925
|
+
const frame = await this.findPluginUIFrame();
|
|
926
|
+
try {
|
|
927
|
+
const result = await frame.evaluate(`window.deleteComponentProperty(${JSON.stringify(nodeId)}, ${JSON.stringify(propertyName)})`);
|
|
928
|
+
logger.info({ success: result.success }, 'Property deleted');
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
logger.error({ error, nodeId, propertyName }, 'Delete component property failed');
|
|
933
|
+
throw error;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// ============================================================================
|
|
937
|
+
// NEW: NODE MANIPULATION
|
|
938
|
+
// ============================================================================
|
|
939
|
+
/**
|
|
940
|
+
* Resize a node
|
|
941
|
+
*/
|
|
942
|
+
async resizeNode(nodeId, width, height, withConstraints = true) {
|
|
943
|
+
logger.info({ nodeId, width, height, withConstraints }, 'Resizing node via plugin UI');
|
|
944
|
+
const frame = await this.findPluginUIFrame();
|
|
945
|
+
try {
|
|
946
|
+
const result = await frame.evaluate(`window.resizeNode(${JSON.stringify(nodeId)}, ${width}, ${height}, ${withConstraints})`);
|
|
947
|
+
logger.info({ success: result.success, nodeId: result.node?.id }, 'Node resized');
|
|
948
|
+
return result;
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
logger.error({ error, nodeId }, 'Resize node failed');
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Move/position a node
|
|
957
|
+
*/
|
|
958
|
+
async moveNode(nodeId, x, y) {
|
|
959
|
+
logger.info({ nodeId, x, y }, 'Moving node via plugin UI');
|
|
960
|
+
const frame = await this.findPluginUIFrame();
|
|
961
|
+
try {
|
|
962
|
+
const result = await frame.evaluate(`window.moveNode(${JSON.stringify(nodeId)}, ${x}, ${y})`);
|
|
963
|
+
logger.info({ success: result.success, nodeId: result.node?.id }, 'Node moved');
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
logger.error({ error, nodeId }, 'Move node failed');
|
|
968
|
+
throw error;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Set fills (colors) on a node
|
|
973
|
+
*/
|
|
974
|
+
async setNodeFills(nodeId, fills) {
|
|
975
|
+
logger.info({ nodeId, fillCount: fills.length }, 'Setting node fills via plugin UI');
|
|
976
|
+
const frame = await this.findPluginUIFrame();
|
|
977
|
+
try {
|
|
978
|
+
const result = await frame.evaluate(`window.setNodeFills(${JSON.stringify(nodeId)}, ${JSON.stringify(fills)})`);
|
|
979
|
+
logger.info({ success: result.success, nodeId: result.node?.id }, 'Fills set');
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
logger.error({ error, nodeId }, 'Set fills failed');
|
|
984
|
+
throw error;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Set strokes on a node
|
|
989
|
+
*/
|
|
990
|
+
async setNodeStrokes(nodeId, strokes, strokeWeight) {
|
|
991
|
+
logger.info({ nodeId, strokeCount: strokes.length, strokeWeight }, 'Setting node strokes via plugin UI');
|
|
992
|
+
const frame = await this.findPluginUIFrame();
|
|
993
|
+
try {
|
|
994
|
+
const result = await frame.evaluate(`window.setNodeStrokes(${JSON.stringify(nodeId)}, ${JSON.stringify(strokes)}, ${strokeWeight})`);
|
|
995
|
+
logger.info({ success: result.success, nodeId: result.node?.id }, 'Strokes set');
|
|
996
|
+
return result;
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
logger.error({ error, nodeId }, 'Set strokes failed');
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Set opacity on a node
|
|
1005
|
+
*/
|
|
1006
|
+
async setNodeOpacity(nodeId, opacity) {
|
|
1007
|
+
logger.info({ nodeId, opacity }, 'Setting node opacity via plugin UI');
|
|
1008
|
+
const frame = await this.findPluginUIFrame();
|
|
1009
|
+
try {
|
|
1010
|
+
const result = await frame.evaluate(`window.setNodeOpacity(${JSON.stringify(nodeId)}, ${opacity})`);
|
|
1011
|
+
logger.info({ success: result.success, opacity: result.node?.opacity }, 'Opacity set');
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
logger.error({ error, nodeId }, 'Set opacity failed');
|
|
1016
|
+
throw error;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Set corner radius on a node
|
|
1021
|
+
*/
|
|
1022
|
+
async setNodeCornerRadius(nodeId, radius) {
|
|
1023
|
+
logger.info({ nodeId, radius }, 'Setting node corner radius via plugin UI');
|
|
1024
|
+
const frame = await this.findPluginUIFrame();
|
|
1025
|
+
try {
|
|
1026
|
+
const result = await frame.evaluate(`window.setNodeCornerRadius(${JSON.stringify(nodeId)}, ${radius})`);
|
|
1027
|
+
logger.info({ success: result.success, radius: result.node?.cornerRadius }, 'Corner radius set');
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
logger.error({ error, nodeId }, 'Set corner radius failed');
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Clone/duplicate a node
|
|
1037
|
+
*/
|
|
1038
|
+
async cloneNode(nodeId) {
|
|
1039
|
+
logger.info({ nodeId }, 'Cloning node via plugin UI');
|
|
1040
|
+
const frame = await this.findPluginUIFrame();
|
|
1041
|
+
try {
|
|
1042
|
+
const result = await frame.evaluate(`window.cloneNode(${JSON.stringify(nodeId)})`);
|
|
1043
|
+
logger.info({ success: result.success, clonedId: result.node?.id }, 'Node cloned');
|
|
1044
|
+
return result;
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
logger.error({ error, nodeId }, 'Clone node failed');
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Delete a node
|
|
1053
|
+
*/
|
|
1054
|
+
async deleteNode(nodeId) {
|
|
1055
|
+
logger.info({ nodeId }, 'Deleting node via plugin UI');
|
|
1056
|
+
const frame = await this.findPluginUIFrame();
|
|
1057
|
+
try {
|
|
1058
|
+
const result = await frame.evaluate(`window.deleteNode(${JSON.stringify(nodeId)})`);
|
|
1059
|
+
logger.info({ success: result.success, deletedName: result.deleted?.name }, 'Node deleted');
|
|
1060
|
+
return result;
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
logger.error({ error, nodeId }, 'Delete node failed');
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Rename a node
|
|
1069
|
+
*/
|
|
1070
|
+
async renameNode(nodeId, newName) {
|
|
1071
|
+
logger.info({ nodeId, newName }, 'Renaming node via plugin UI');
|
|
1072
|
+
const frame = await this.findPluginUIFrame();
|
|
1073
|
+
try {
|
|
1074
|
+
const result = await frame.evaluate(`window.renameNode(${JSON.stringify(nodeId)}, ${JSON.stringify(newName)})`);
|
|
1075
|
+
logger.info({ success: result.success, oldName: result.node?.oldName, newName: result.node?.name }, 'Node renamed');
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
logger.error({ error, nodeId }, 'Rename node failed');
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Set text content on a text node
|
|
1085
|
+
*/
|
|
1086
|
+
async setTextContent(nodeId, text, options) {
|
|
1087
|
+
logger.info({ nodeId, textLength: text.length }, 'Setting text content via plugin UI');
|
|
1088
|
+
const frame = await this.findPluginUIFrame();
|
|
1089
|
+
try {
|
|
1090
|
+
const result = await frame.evaluate(`window.setTextContent(${JSON.stringify(nodeId)}, ${JSON.stringify(text)}, ${JSON.stringify(options || {})})`);
|
|
1091
|
+
logger.info({ success: result.success, characters: result.node?.characters?.substring(0, 50) }, 'Text content set');
|
|
1092
|
+
return result;
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
logger.error({ error, nodeId }, 'Set text content failed');
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Create a child node
|
|
1101
|
+
*/
|
|
1102
|
+
async createChildNode(parentId, nodeType, properties) {
|
|
1103
|
+
logger.info({ parentId, nodeType, properties }, 'Creating child node via plugin UI');
|
|
1104
|
+
const frame = await this.findPluginUIFrame();
|
|
1105
|
+
try {
|
|
1106
|
+
const result = await frame.evaluate(`window.createChildNode(${JSON.stringify(parentId)}, ${JSON.stringify(nodeType)}, ${JSON.stringify(properties || {})})`);
|
|
1107
|
+
logger.info({ success: result.success, nodeId: result.node?.id, nodeType: result.node?.type }, 'Child node created');
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
catch (error) {
|
|
1111
|
+
logger.error({ error, parentId, nodeType }, 'Create child node failed');
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// ============================================================================
|
|
1116
|
+
// SCREENSHOT & INSTANCE PROPERTIES (via plugin UI)
|
|
1117
|
+
// ============================================================================
|
|
1118
|
+
/**
|
|
1119
|
+
* Capture screenshot via plugin's exportAsync
|
|
1120
|
+
*/
|
|
1121
|
+
async captureScreenshot(nodeId, options) {
|
|
1122
|
+
logger.info({ nodeId, options }, 'Capturing screenshot via plugin UI');
|
|
1123
|
+
const frame = await this.findPluginUIFrame();
|
|
1124
|
+
try {
|
|
1125
|
+
const result = await frame.evaluate(`window.captureScreenshot(${JSON.stringify(nodeId)}, ${JSON.stringify(options || {})})`);
|
|
1126
|
+
logger.info({ success: result.success }, 'Screenshot captured');
|
|
1127
|
+
return result;
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
logger.error({ error, nodeId }, 'Capture screenshot failed');
|
|
1131
|
+
throw error;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Set component instance properties
|
|
1136
|
+
*/
|
|
1137
|
+
async setInstanceProperties(nodeId, properties) {
|
|
1138
|
+
logger.info({ nodeId, properties: Object.keys(properties || {}) }, 'Setting instance properties via plugin UI');
|
|
1139
|
+
const frame = await this.findPluginUIFrame();
|
|
1140
|
+
try {
|
|
1141
|
+
const result = await frame.evaluate(`window.setInstanceProperties(${JSON.stringify(nodeId)}, ${JSON.stringify(properties)})`);
|
|
1142
|
+
logger.info({ success: result.success }, 'Instance properties set');
|
|
1143
|
+
return result;
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
logger.error({ error, nodeId }, 'Set instance properties failed');
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Set image fill on one or more nodes (decodes base64 in browser bridge, sends bytes to plugin)
|
|
1152
|
+
*/
|
|
1153
|
+
async setImageFill(nodeIds, imageData, scaleMode = 'FILL') {
|
|
1154
|
+
logger.info({ nodeIds, scaleMode, dataLength: imageData.length }, 'Setting image fill via plugin UI');
|
|
1155
|
+
const frame = await this.findPluginUIFrame();
|
|
1156
|
+
try {
|
|
1157
|
+
const result = await frame.evaluate(`window.setImageFill(${JSON.stringify(nodeIds)}, ${JSON.stringify(imageData)}, ${JSON.stringify(scaleMode)})`);
|
|
1158
|
+
logger.info({ success: result.success, imageHash: result.imageHash }, 'Image fill set');
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
logger.error({ error, nodeIds }, 'Set image fill failed');
|
|
1163
|
+
throw error;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Lint design for accessibility and quality issues via plugin UI
|
|
1168
|
+
*/
|
|
1169
|
+
async lintDesign(nodeId, rules, maxDepth, maxFindings) {
|
|
1170
|
+
logger.info({ nodeId, rules }, 'Linting design via plugin UI');
|
|
1171
|
+
const frame = await this.findPluginUIFrame();
|
|
1172
|
+
try {
|
|
1173
|
+
const result = await frame.evaluate(`window.lintDesign(${JSON.stringify(nodeId || null)}, ${JSON.stringify(rules || null)}, ${JSON.stringify(maxDepth ?? null)}, ${JSON.stringify(maxFindings ?? null)})`);
|
|
1174
|
+
logger.info({ success: result.success }, 'Design lint complete');
|
|
1175
|
+
return result;
|
|
1176
|
+
}
|
|
1177
|
+
catch (error) {
|
|
1178
|
+
logger.error({ error, nodeId }, 'Design lint failed');
|
|
1179
|
+
throw error;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
FigmaDesktopConnector.DEBUG = process.env.DEBUG === '1' || process.env.DEBUG === 'true';
|
|
1184
|
+
//# sourceMappingURL=figma-desktop-connector.js.map
|