@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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Browser Manager (Legacy)
|
|
3
|
+
* Note: This module is maintained for backwards compatibility but is no longer
|
|
4
|
+
* the primary connection method. Use the WebSocket Desktop Bridge plugin instead.
|
|
5
|
+
*/
|
|
6
|
+
import puppeteer from 'puppeteer-core';
|
|
7
|
+
import { createChildLogger } from '../core/logger.js';
|
|
8
|
+
import { extractFileKey } from '../core/figma-api.js';
|
|
9
|
+
const logger = createChildLogger({ component: 'local-browser' });
|
|
10
|
+
/**
|
|
11
|
+
* Local Browser Manager
|
|
12
|
+
* Connects to existing Figma Desktop instance via remote debugging port
|
|
13
|
+
*/
|
|
14
|
+
export class LocalBrowserManager {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.browser = null;
|
|
17
|
+
this.page = null;
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Connect to Figma Desktop via remote debugging port
|
|
22
|
+
*/
|
|
23
|
+
async launch() {
|
|
24
|
+
if (this.browser) {
|
|
25
|
+
logger.info('Browser already connected, reusing instance');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const { debugHost, debugPort } = this.config;
|
|
29
|
+
const browserURL = `http://${debugHost}:${debugPort}`;
|
|
30
|
+
logger.info({ browserURL }, 'Connecting to Figma Desktop');
|
|
31
|
+
try {
|
|
32
|
+
// Connect to existing browser (Figma Desktop)
|
|
33
|
+
this.browser = await puppeteer.connect({
|
|
34
|
+
browserURL,
|
|
35
|
+
defaultViewport: null, // Use Figma's viewport
|
|
36
|
+
});
|
|
37
|
+
logger.info('Connected to Figma Desktop successfully');
|
|
38
|
+
// Handle disconnection
|
|
39
|
+
this.browser.on('disconnected', () => {
|
|
40
|
+
logger.warn('Disconnected from Figma Desktop');
|
|
41
|
+
this.browser = null;
|
|
42
|
+
this.page = null;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error({ error, browserURL }, 'Failed to connect to Figma Desktop');
|
|
47
|
+
throw new Error(`Failed to connect to Figma Desktop.\n\n` +
|
|
48
|
+
`Please open the Desktop Bridge plugin in Figma:\n` +
|
|
49
|
+
` Plugins → Development → Figma Desktop Bridge\n\n` +
|
|
50
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Find the best page for plugin debugging
|
|
55
|
+
* Actively searches for pages with workers across ALL tabs
|
|
56
|
+
*/
|
|
57
|
+
async findBestPage() {
|
|
58
|
+
if (!this.browser) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const pages = await this.browser.pages();
|
|
62
|
+
// Find Figma pages with workers
|
|
63
|
+
const figmaPages = pages.filter(p => {
|
|
64
|
+
const url = p.url();
|
|
65
|
+
return url.includes('figma.com') && !url.includes('devtools');
|
|
66
|
+
});
|
|
67
|
+
if (figmaPages.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// Check each page for workers
|
|
71
|
+
const pagesWithWorkers = figmaPages
|
|
72
|
+
.map(p => ({
|
|
73
|
+
page: p,
|
|
74
|
+
workerCount: p.workers().length,
|
|
75
|
+
url: p.url()
|
|
76
|
+
}))
|
|
77
|
+
.filter(p => p.workerCount > 0)
|
|
78
|
+
.sort((a, b) => b.workerCount - a.workerCount); // Most workers first
|
|
79
|
+
if (pagesWithWorkers.length > 0) {
|
|
80
|
+
logger.info({
|
|
81
|
+
url: pagesWithWorkers[0].url,
|
|
82
|
+
workerCount: pagesWithWorkers[0].workerCount,
|
|
83
|
+
totalPagesWithWorkers: pagesWithWorkers.length
|
|
84
|
+
}, 'Found page with active plugin workers');
|
|
85
|
+
return pagesWithWorkers[0].page;
|
|
86
|
+
}
|
|
87
|
+
// No workers found - prefer design/file pages
|
|
88
|
+
const designPage = figmaPages.find(p => p.url().includes('/design/') || p.url().includes('/file/'));
|
|
89
|
+
return designPage || figmaPages[0];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Find an existing browser tab whose URL matches the given Figma file key
|
|
93
|
+
*/
|
|
94
|
+
async findPageByFileKey(targetFileKey) {
|
|
95
|
+
if (!this.browser) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const pages = await this.browser.pages();
|
|
99
|
+
for (const page of pages) {
|
|
100
|
+
const pageUrl = page.url();
|
|
101
|
+
if (!pageUrl.includes('figma.com') || pageUrl.includes('devtools')) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const pageFileKey = extractFileKey(pageUrl);
|
|
105
|
+
if (pageFileKey && pageFileKey === targetFileKey) {
|
|
106
|
+
logger.info({ url: pageUrl, fileKey: pageFileKey }, 'Found existing tab for file key');
|
|
107
|
+
return page;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get active Figma page or create new one
|
|
114
|
+
* Prefers pages with active plugin workers for plugin debugging
|
|
115
|
+
*/
|
|
116
|
+
async getPage() {
|
|
117
|
+
// Ensure connection is alive before proceeding
|
|
118
|
+
await this.ensureConnection();
|
|
119
|
+
if (!this.browser) {
|
|
120
|
+
await this.launch();
|
|
121
|
+
}
|
|
122
|
+
// If we already have a page from explicit navigation, use it (don't override with findBestPage)
|
|
123
|
+
if (this.page && !this.page.isClosed()) {
|
|
124
|
+
return this.page;
|
|
125
|
+
}
|
|
126
|
+
// No explicit page set — find the best page (most workers) for initial connection
|
|
127
|
+
const bestPage = await this.findBestPage();
|
|
128
|
+
if (bestPage) {
|
|
129
|
+
const workerCount = bestPage.workers().length;
|
|
130
|
+
logger.info({
|
|
131
|
+
url: bestPage.url(),
|
|
132
|
+
workerCount
|
|
133
|
+
}, 'Selected page for monitoring (auto-detected)');
|
|
134
|
+
this.page = bestPage;
|
|
135
|
+
return this.page;
|
|
136
|
+
}
|
|
137
|
+
// Fallback: Get any existing page or create new one
|
|
138
|
+
const pages = await this.browser.pages();
|
|
139
|
+
if (pages.length > 0 && pages[0].url() !== 'about:blank') {
|
|
140
|
+
logger.warn({ url: pages[0].url() }, 'No Figma pages found, using first available page');
|
|
141
|
+
this.page = pages[0];
|
|
142
|
+
return this.page;
|
|
143
|
+
}
|
|
144
|
+
// Last resort: Create new page
|
|
145
|
+
logger.warn('No suitable pages found, creating new page in Figma Desktop');
|
|
146
|
+
this.page = await this.browser.newPage();
|
|
147
|
+
return this.page;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Navigate to Figma URL
|
|
151
|
+
* If the target file is already open in a tab, switches to it instead of navigating.
|
|
152
|
+
*/
|
|
153
|
+
async navigateToFigma(figmaUrl) {
|
|
154
|
+
// Ensure connection is alive before navigation
|
|
155
|
+
await this.ensureConnection();
|
|
156
|
+
// Default to Figma homepage if no URL provided
|
|
157
|
+
const url = figmaUrl || 'https://www.figma.com';
|
|
158
|
+
// Check if the target file is already open in an existing tab
|
|
159
|
+
const targetFileKey = extractFileKey(url);
|
|
160
|
+
if (targetFileKey) {
|
|
161
|
+
const existingPage = await this.findPageByFileKey(targetFileKey);
|
|
162
|
+
if (existingPage) {
|
|
163
|
+
logger.info({ url, fileKey: targetFileKey }, 'Switching to existing tab instead of navigating');
|
|
164
|
+
await existingPage.bringToFront();
|
|
165
|
+
this.page = existingPage;
|
|
166
|
+
return { page: existingPage, action: 'switched_to_existing', url: existingPage.url() };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// No existing tab found — fall through to normal navigation
|
|
170
|
+
const page = await this.getPage();
|
|
171
|
+
logger.info({ url }, 'Navigating to Figma');
|
|
172
|
+
try {
|
|
173
|
+
await page.goto(url, {
|
|
174
|
+
waitUntil: 'networkidle2',
|
|
175
|
+
timeout: 30000,
|
|
176
|
+
});
|
|
177
|
+
logger.info({ url }, 'Navigation successful');
|
|
178
|
+
return { page, action: 'navigated', url };
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
logger.error({ error, url }, 'Navigation failed');
|
|
182
|
+
throw new Error(`Failed to navigate to ${url}: ${error}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Reload current page
|
|
187
|
+
*/
|
|
188
|
+
async reload(hardReload = false) {
|
|
189
|
+
if (!this.page || this.page.isClosed()) {
|
|
190
|
+
throw new Error('No active page to reload');
|
|
191
|
+
}
|
|
192
|
+
logger.info({ hardReload }, 'Reloading page');
|
|
193
|
+
try {
|
|
194
|
+
await this.page.reload({
|
|
195
|
+
waitUntil: 'networkidle2',
|
|
196
|
+
timeout: 30000,
|
|
197
|
+
});
|
|
198
|
+
logger.info('Page reloaded successfully');
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
logger.error({ error }, 'Page reload failed');
|
|
202
|
+
throw new Error(`Page reload failed: ${error}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Execute JavaScript in page context
|
|
207
|
+
*/
|
|
208
|
+
async evaluate(fn) {
|
|
209
|
+
const page = await this.getPage();
|
|
210
|
+
return page.evaluate(fn);
|
|
211
|
+
}
|
|
212
|
+
// Screenshot functionality removed - use Figma REST API's getImages() instead
|
|
213
|
+
// See: figma_take_screenshot and figma_get_component_image tools
|
|
214
|
+
/**
|
|
215
|
+
* Check if browser is connected
|
|
216
|
+
*/
|
|
217
|
+
isRunning() {
|
|
218
|
+
return this.browser !== null && this.browser.isConnected();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Disconnect from browser (doesn't close Figma Desktop)
|
|
222
|
+
*/
|
|
223
|
+
async close() {
|
|
224
|
+
if (!this.browser) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
logger.info('Disconnecting from Figma Desktop');
|
|
228
|
+
try {
|
|
229
|
+
// Just disconnect, don't close Figma Desktop
|
|
230
|
+
this.browser.disconnect();
|
|
231
|
+
this.browser = null;
|
|
232
|
+
this.page = null;
|
|
233
|
+
logger.info('Disconnected from Figma Desktop successfully');
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
logger.error({ error }, 'Failed to disconnect from browser');
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get current page URL
|
|
242
|
+
*/
|
|
243
|
+
getCurrentUrl() {
|
|
244
|
+
if (!this.page || this.page.isClosed()) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return this.page.url();
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Check if the browser connection is still alive
|
|
251
|
+
* Returns false if connection is stale (e.g., after computer sleep)
|
|
252
|
+
*/
|
|
253
|
+
async isConnectionAlive() {
|
|
254
|
+
try {
|
|
255
|
+
if (!this.browser || !this.page) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
// Try to get the page title - this will fail if connection is dead
|
|
259
|
+
await this.page.title();
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
logger.warn({ error }, 'Browser connection appears to be dead');
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Reconnect to Figma Desktop if connection was lost
|
|
269
|
+
* Call this before any operation that requires a live connection
|
|
270
|
+
*/
|
|
271
|
+
async ensureConnection() {
|
|
272
|
+
const isAlive = await this.isConnectionAlive();
|
|
273
|
+
if (!isAlive) {
|
|
274
|
+
logger.info('Connection lost, attempting to reconnect to Figma Desktop');
|
|
275
|
+
// Clear stale references
|
|
276
|
+
this.browser = null;
|
|
277
|
+
this.page = null;
|
|
278
|
+
// Reconnect
|
|
279
|
+
await this.launch();
|
|
280
|
+
logger.info('Successfully reconnected to Figma Desktop');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Force a complete reconnection to Figma Desktop
|
|
285
|
+
* Use this when frames become detached or stale even though the browser appears connected
|
|
286
|
+
*/
|
|
287
|
+
async forceReconnect() {
|
|
288
|
+
logger.info('Force reconnecting to Figma Desktop');
|
|
289
|
+
// Disconnect current connection if exists
|
|
290
|
+
if (this.browser) {
|
|
291
|
+
try {
|
|
292
|
+
this.browser.disconnect();
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
// Ignore disconnect errors
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Clear all references
|
|
299
|
+
this.browser = null;
|
|
300
|
+
this.page = null;
|
|
301
|
+
// Reconnect
|
|
302
|
+
await this.launch();
|
|
303
|
+
logger.info('Force reconnect completed');
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Wait for navigation
|
|
307
|
+
*/
|
|
308
|
+
async waitForNavigation(timeout = 30000) {
|
|
309
|
+
if (!this.page || this.page.isClosed()) {
|
|
310
|
+
throw new Error('No active page');
|
|
311
|
+
}
|
|
312
|
+
await this.page.waitForNavigation({
|
|
313
|
+
waitUntil: 'networkidle2',
|
|
314
|
+
timeout,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/browser/local.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility Scorer (weight: 0.15)
|
|
3
|
+
*
|
|
4
|
+
* Checks accessibility-related signals in the design system.
|
|
5
|
+
* Evaluates color contrast ratios, state variant coverage,
|
|
6
|
+
* and semantic color naming patterns.
|
|
7
|
+
*/
|
|
8
|
+
import { clamp, getSeverity } from "./types.js";
|
|
9
|
+
/** Maximum examples to include in a finding. */
|
|
10
|
+
const MAX_EXAMPLES = 5;
|
|
11
|
+
/** WCAG AA minimum contrast ratio for normal text. */
|
|
12
|
+
const WCAG_AA_RATIO = 4.5;
|
|
13
|
+
/** State-related variant values that indicate accessible component design. */
|
|
14
|
+
const STATE_VARIANTS = [
|
|
15
|
+
"disabled",
|
|
16
|
+
"error",
|
|
17
|
+
"focus",
|
|
18
|
+
"hover",
|
|
19
|
+
"active",
|
|
20
|
+
"pressed",
|
|
21
|
+
"selected",
|
|
22
|
+
];
|
|
23
|
+
/** Semantic color token name patterns that indicate accessibility awareness. */
|
|
24
|
+
const SEMANTIC_COLOR_NAMES = ["error", "warning", "success", "info", "danger"];
|
|
25
|
+
/**
|
|
26
|
+
* Linearize an sRGB channel value for luminance calculation.
|
|
27
|
+
* Input: channel value in 0-1 range.
|
|
28
|
+
*/
|
|
29
|
+
function linearize(channel) {
|
|
30
|
+
return channel <= 0.04045
|
|
31
|
+
? channel / 12.92
|
|
32
|
+
: ((channel + 0.055) / 1.055) ** 2.4;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Calculate relative luminance of a color.
|
|
36
|
+
* r, g, b are in 0-1 range (as Figma provides them).
|
|
37
|
+
*/
|
|
38
|
+
function luminance(r, g, b) {
|
|
39
|
+
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Calculate contrast ratio between two colors.
|
|
43
|
+
* Returns a ratio >= 1 (e.g., 4.5 for WCAG AA compliance).
|
|
44
|
+
*/
|
|
45
|
+
function contrastRatio(r1, g1, b1, r2, g2, b2) {
|
|
46
|
+
const lum1 = luminance(r1, g1, b1);
|
|
47
|
+
const lum2 = luminance(r2, g2, b2);
|
|
48
|
+
const lighter = Math.max(lum1, lum2);
|
|
49
|
+
const darker = Math.min(lum1, lum2);
|
|
50
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if a value is a direct color (not an alias).
|
|
54
|
+
*/
|
|
55
|
+
function isDirectColor(value) {
|
|
56
|
+
if (typeof value !== "object" || value === null)
|
|
57
|
+
return false;
|
|
58
|
+
const v = value;
|
|
59
|
+
return (typeof v.r === "number" &&
|
|
60
|
+
typeof v.g === "number" &&
|
|
61
|
+
typeof v.b === "number" &&
|
|
62
|
+
v.type !== "VARIABLE_ALIAS");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extract resolved color values from variables.
|
|
66
|
+
* Returns an array of { name, r, g, b } objects for direct color values.
|
|
67
|
+
*/
|
|
68
|
+
function extractColorValues(variables) {
|
|
69
|
+
const colors = [];
|
|
70
|
+
for (const variable of variables) {
|
|
71
|
+
if (variable.resolvedType !== "COLOR" || !variable.valuesByMode)
|
|
72
|
+
continue;
|
|
73
|
+
for (const value of Object.values(variable.valuesByMode)) {
|
|
74
|
+
if (isDirectColor(value)) {
|
|
75
|
+
colors.push({
|
|
76
|
+
name: variable.name,
|
|
77
|
+
r: value.r,
|
|
78
|
+
g: value.g,
|
|
79
|
+
b: value.b,
|
|
80
|
+
});
|
|
81
|
+
break; // Use the first direct color value per variable
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return colors;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Identify likely foreground/background color pairs and check contrast.
|
|
89
|
+
*
|
|
90
|
+
* Strategy: pair colors whose names suggest fg/bg relationships:
|
|
91
|
+
* - Names containing "background"/"bg"/"surface" are backgrounds
|
|
92
|
+
* - Names containing "text"/"foreground"/"fg"/"on" are foregrounds
|
|
93
|
+
* Falls back to pairing dark colors with light colors if no naming convention found.
|
|
94
|
+
*/
|
|
95
|
+
function scoreColorContrast(data) {
|
|
96
|
+
const colors = extractColorValues(data.variables);
|
|
97
|
+
if (colors.length < 2) {
|
|
98
|
+
return {
|
|
99
|
+
id: "a11y-color-contrast",
|
|
100
|
+
label: "Color contrast",
|
|
101
|
+
score: 100,
|
|
102
|
+
severity: "info",
|
|
103
|
+
tooltip: "Foreground/background color pairs should meet WCAG AA contrast ratio (4.5:1). Low contrast makes content unreadable for users with vision impairments.",
|
|
104
|
+
details: colors.length === 0
|
|
105
|
+
? "No direct color values to evaluate."
|
|
106
|
+
: "Only one color found; need at least two to check contrast.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const bgPattern = /background|bg|surface|canvas|base/i;
|
|
110
|
+
const fgPattern = /text|foreground|fg|on-|on\.|label|title|body|heading/i;
|
|
111
|
+
const backgrounds = colors.filter((c) => bgPattern.test(c.name));
|
|
112
|
+
const foregrounds = colors.filter((c) => fgPattern.test(c.name));
|
|
113
|
+
// If naming conventions are not used, use luminance-based heuristic
|
|
114
|
+
let pairs = [];
|
|
115
|
+
if (backgrounds.length > 0 && foregrounds.length > 0) {
|
|
116
|
+
// Pair foregrounds with backgrounds by naming proximity
|
|
117
|
+
for (const fg of foregrounds) {
|
|
118
|
+
for (const bg of backgrounds) {
|
|
119
|
+
pairs.push({ fg, bg });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Heuristic: separate into light (luminance > 0.5) and dark (luminance <= 0.5)
|
|
125
|
+
const lightColors = colors.filter((c) => luminance(c.r, c.g, c.b) > 0.5);
|
|
126
|
+
const darkColors = colors.filter((c) => luminance(c.r, c.g, c.b) <= 0.5);
|
|
127
|
+
for (const dark of darkColors) {
|
|
128
|
+
for (const light of lightColors) {
|
|
129
|
+
pairs.push({ fg: dark, bg: light });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (pairs.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
id: "a11y-color-contrast",
|
|
136
|
+
label: "Color contrast",
|
|
137
|
+
score: 50,
|
|
138
|
+
severity: "warning",
|
|
139
|
+
tooltip: "Foreground/background color pairs should meet WCAG AA contrast ratio (4.5:1). Low contrast makes content unreadable for users with vision impairments.",
|
|
140
|
+
details: "Could not identify foreground/background color pairs to check contrast.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Limit pair evaluation to avoid performance issues with large token sets
|
|
144
|
+
const maxPairs = 50;
|
|
145
|
+
if (pairs.length > maxPairs) {
|
|
146
|
+
pairs = pairs.slice(0, maxPairs);
|
|
147
|
+
}
|
|
148
|
+
let passingPairs = 0;
|
|
149
|
+
const failingExamples = [];
|
|
150
|
+
for (const { fg, bg } of pairs) {
|
|
151
|
+
const ratio = contrastRatio(fg.r, fg.g, fg.b, bg.r, bg.g, bg.b);
|
|
152
|
+
if (ratio >= WCAG_AA_RATIO) {
|
|
153
|
+
passingPairs++;
|
|
154
|
+
}
|
|
155
|
+
else if (failingExamples.length < MAX_EXAMPLES) {
|
|
156
|
+
failingExamples.push(`${fg.name} / ${bg.name} (${ratio.toFixed(1)}:1)`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const passRatio = passingPairs / pairs.length;
|
|
160
|
+
const score = clamp(passRatio * 100);
|
|
161
|
+
return {
|
|
162
|
+
id: "a11y-color-contrast",
|
|
163
|
+
label: "Color contrast",
|
|
164
|
+
score,
|
|
165
|
+
severity: getSeverity(score),
|
|
166
|
+
tooltip: "Foreground/background color pairs should meet WCAG AA contrast ratio (4.5:1). Low contrast makes content unreadable for users with vision impairments.",
|
|
167
|
+
details: `${passingPairs} of ${pairs.length} color pairs meet WCAG AA contrast ratio (${WCAG_AA_RATIO}:1).`,
|
|
168
|
+
examples: failingExamples.length > 0 ? failingExamples : undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Score state variant coverage.
|
|
173
|
+
* Components should include state-related variants for accessibility.
|
|
174
|
+
*/
|
|
175
|
+
function scoreStateVariants(data) {
|
|
176
|
+
const components = data.components;
|
|
177
|
+
if (components.length === 0) {
|
|
178
|
+
return {
|
|
179
|
+
id: "a11y-state-variants",
|
|
180
|
+
label: "State variants",
|
|
181
|
+
score: 100,
|
|
182
|
+
severity: "info",
|
|
183
|
+
tooltip: "Interactive components should include state variants (disabled, error, focus, hover, active, pressed, selected) for accessible interactions.",
|
|
184
|
+
details: "No components to evaluate.",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Check which state variants exist across all component names
|
|
188
|
+
const allNames = components.map((c) => c.name.toLowerCase()).join(" ");
|
|
189
|
+
const foundStates = STATE_VARIANTS.filter((state) => allNames.includes(state));
|
|
190
|
+
const ratio = foundStates.length / STATE_VARIANTS.length;
|
|
191
|
+
const score = clamp(ratio * 100);
|
|
192
|
+
const missingStates = STATE_VARIANTS.filter((state) => !allNames.includes(state));
|
|
193
|
+
return {
|
|
194
|
+
id: "a11y-state-variants",
|
|
195
|
+
label: "State variants",
|
|
196
|
+
score,
|
|
197
|
+
severity: getSeverity(score),
|
|
198
|
+
tooltip: "Interactive components should include state variants (disabled, error, focus, hover, active, pressed, selected) for accessible interactions.",
|
|
199
|
+
details: missingStates.length > 0
|
|
200
|
+
? `Found ${foundStates.length} of ${STATE_VARIANTS.length} state variants. Missing: ${missingStates.join(", ")}.`
|
|
201
|
+
: `All ${STATE_VARIANTS.length} state variants are represented.`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Score semantic color naming.
|
|
206
|
+
* The token set should include semantic color tokens for error/warning/success/info.
|
|
207
|
+
*/
|
|
208
|
+
function variableDataUnavailable(data) {
|
|
209
|
+
return (data.dataAvailability !== undefined && !data.dataAvailability.variables);
|
|
210
|
+
}
|
|
211
|
+
function scoreSemanticColorNaming(data) {
|
|
212
|
+
const colorVars = data.variables.filter((v) => v.resolvedType === "COLOR");
|
|
213
|
+
if (colorVars.length === 0) {
|
|
214
|
+
const unavailable = variableDataUnavailable(data);
|
|
215
|
+
return {
|
|
216
|
+
id: "a11y-semantic-colors",
|
|
217
|
+
label: "Semantic color naming",
|
|
218
|
+
score: 0,
|
|
219
|
+
severity: unavailable ? "info" : "fail",
|
|
220
|
+
tooltip: "The token set should include semantic color categories (error, warning, success, info, danger) to convey meaning beyond color alone.",
|
|
221
|
+
details: unavailable
|
|
222
|
+
? `Variable data unavailable: ${data.dataAvailability?.variableError || "Requires Desktop Bridge or Enterprise plan."}`
|
|
223
|
+
: "No color variables found.",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const foundSemantic = [];
|
|
227
|
+
const missingSemantic = [];
|
|
228
|
+
const semanticExamples = [];
|
|
229
|
+
for (const name of SEMANTIC_COLOR_NAMES) {
|
|
230
|
+
const matching = colorVars.filter((v) => v.name.toLowerCase().includes(name));
|
|
231
|
+
if (matching.length > 0) {
|
|
232
|
+
foundSemantic.push(name);
|
|
233
|
+
semanticExamples.push(`${name}: ${matching
|
|
234
|
+
.slice(0, 2)
|
|
235
|
+
.map((v) => v.name)
|
|
236
|
+
.join(", ")}`);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
missingSemantic.push(name);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const ratio = foundSemantic.length / SEMANTIC_COLOR_NAMES.length;
|
|
243
|
+
const score = clamp(ratio * 100);
|
|
244
|
+
return {
|
|
245
|
+
id: "a11y-semantic-colors",
|
|
246
|
+
label: "Semantic color naming",
|
|
247
|
+
score,
|
|
248
|
+
severity: getSeverity(score),
|
|
249
|
+
tooltip: "The token set should include semantic color categories (error, warning, success, info, danger) to convey meaning beyond color alone.",
|
|
250
|
+
details: missingSemantic.length > 0
|
|
251
|
+
? `Found ${foundSemantic.length} of ${SEMANTIC_COLOR_NAMES.length} semantic color categories. Missing: ${missingSemantic.join(", ")}.`
|
|
252
|
+
: "All semantic color categories (error, warning, success, info, danger) are present.",
|
|
253
|
+
examples: semanticExamples.length > 0
|
|
254
|
+
? semanticExamples.slice(0, MAX_EXAMPLES)
|
|
255
|
+
: undefined,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Accessibility category scorer.
|
|
260
|
+
* Returns the average score across all accessibility checks.
|
|
261
|
+
*/
|
|
262
|
+
export function scoreAccessibility(data) {
|
|
263
|
+
const findings = [
|
|
264
|
+
scoreColorContrast(data),
|
|
265
|
+
scoreStateVariants(data),
|
|
266
|
+
scoreSemanticColorNaming(data),
|
|
267
|
+
];
|
|
268
|
+
const score = clamp(findings.reduce((sum, f) => sum + f.score, 0) / findings.length);
|
|
269
|
+
return {
|
|
270
|
+
id: "accessibility",
|
|
271
|
+
label: "Accessibility",
|
|
272
|
+
shortLabel: "Accessibility",
|
|
273
|
+
score,
|
|
274
|
+
weight: 0.15,
|
|
275
|
+
findings,
|
|
276
|
+
};
|
|
277
|
+
}
|