@pedroab0/skills 1.0.0 → 1.1.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 +44 -7
- package/bin/cli.js +152 -32
- package/figma-extractor/README.md +25 -7
- package/figma-extractor/SKILL.md +19 -17
- package/figma-extractor/figma-plugin/README.md +25 -9
- package/figma-extractor/figma-plugin/assets/cover-1920.png +0 -0
- package/figma-extractor/figma-plugin/assets/cover.jpg +0 -0
- package/figma-extractor/figma-plugin/assets/cover.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon-128.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon.png +0 -0
- package/figma-extractor/figma-plugin/manifest.json +8 -2
- package/figma-extractor/scripts/fetch_figma.js +180 -26
- package/figma-extractor/scripts/test_fetch_figma.js +90 -4
- package/figma-node-builder/README.md +2 -2
- package/figma-node-builder/SKILL.md +7 -8
- package/figma-node-builder/scripts/inspect_spec.js +15 -2
- package/figma-node-builder/scripts/test_inspect_spec.js +16 -1
- package/figma-to-code/README.md +10 -4
- package/figma-to-code/SKILL.md +18 -16
- package/figma-to-code/scripts/fetch_figma.js +163 -18
- package/figma-to-code/scripts/test_fetch_figma.js +79 -0
- package/package.json +8 -3
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/icon.png" alt="Figma Node JSON Extractor Icon" width="256" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# Figma Node JSON Extractor
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.figma.com/community/plugin/1691244420692136789/figma-node-json-extractor"><img src="https://img.shields.io/badge/Figma_Community-Figma_Node_JSON_Extractor-000000?style=flat&logo=figma&logoColor=white" alt="Figma Community Plugin" /></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@pedroab0/skills"><img src="https://img.shields.io/npm/v/@pedroab0/skills.svg?label=@pedroab0/skills" alt="npm package" /></a>
|
|
10
|
+
<a href="../../LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
Inspect, copy, and export compact, design-system-aware Node JSON and high-resolution companion preview images directly from **Figma Desktop** for AI coding assistants (Claude Code, Kiro, Cursor, OpenCode, Antigravity) and frontend engineering.
|
|
4
14
|
|
|
5
15
|
---
|
|
6
16
|
|
|
@@ -16,24 +26,30 @@ Inspect, copy, and export compact, design-system-aware Node JSON and high-resolu
|
|
|
16
26
|
## Installation in Figma
|
|
17
27
|
|
|
18
28
|
### Option A: Install from Figma Community (Recommended)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
Install directly from the Figma Community:
|
|
30
|
+
👉 [**Install Figma Node JSON Extractor on Figma Community**](https://www.figma.com/community/plugin/1691244420692136789/figma-node-json-extractor)
|
|
31
|
+
|
|
32
|
+
1. Open the [plugin page on Figma Community](https://www.figma.com/community/plugin/1691244420692136789/figma-node-json-extractor) or search for **Figma Node JSON Extractor** in Figma Plugins.
|
|
33
|
+
2. Click **Open in...** or **Save** to add it to your Figma account.
|
|
23
34
|
|
|
24
35
|
### Option B: Manual Installation (Local / Offline Backup)
|
|
25
36
|
To install the plugin directly from your local skills folder or repository:
|
|
26
37
|
1. Open the **Figma Desktop app**.
|
|
27
38
|
2. In the top-left menu, navigate to:
|
|
28
39
|
$$\text{Plugins} \longrightarrow \text{Development} \longrightarrow \text{Import plugin from manifest...}$$
|
|
29
|
-
3. Select `manifest.json` from your machine based on your setup:
|
|
40
|
+
3. Select `manifest.json` from your machine based on your setup (`~` corresponds to `%USERPROFILE%` on Windows):
|
|
30
41
|
* **Installed via `npx @pedroab0/skills` (Global)**:
|
|
31
42
|
* **Claude Code**: `~/.claude/skills/figma-extractor/figma-plugin/manifest.json`
|
|
32
|
-
* **
|
|
43
|
+
* **Cursor**: `~/.cursor/skills/figma-extractor/figma-plugin/manifest.json`
|
|
33
44
|
* **OpenCode**: `~/.config/opencode/skills/figma-extractor/figma-plugin/manifest.json`
|
|
45
|
+
* **Google Antigravity**: `~/.gemini/config/skills/figma-extractor/figma-plugin/manifest.json`
|
|
46
|
+
* **Kiro**: `~/.kiro/skills/figma-extractor/figma-plugin/manifest.json`
|
|
34
47
|
* **Installed in local workspace (`--local`)**:
|
|
35
48
|
* `.claude/skills/figma-extractor/figma-plugin/manifest.json`
|
|
49
|
+
* `.cursor/skills/figma-extractor/figma-plugin/manifest.json`
|
|
50
|
+
* `.opencode/skills/figma-extractor/figma-plugin/manifest.json`
|
|
36
51
|
* `.gemini/skills/figma-extractor/figma-plugin/manifest.json`
|
|
52
|
+
* `.kiro/skills/figma-extractor/figma-plugin/manifest.json`
|
|
37
53
|
* **Cloned Git repository**:
|
|
38
54
|
* `<repo-root>/figma-extractor/figma-plugin/manifest.json`
|
|
39
55
|
4. The plugin is now permanently loaded under **Plugins** $\rightarrow$ **Development**.
|
|
@@ -43,7 +59,7 @@ To install the plugin directly from your local skills folder or repository:
|
|
|
43
59
|
## How to Extract Node JSON
|
|
44
60
|
|
|
45
61
|
1. **Select any frame or component** on your canvas (e.g., Transaction Card, Navbar, Modal).
|
|
46
|
-
2. Right-click the element $\rightarrow$ **Plugins** $\rightarrow$ **Figma Node JSON Extractor** (or press `⌥ ⌘ P` to re-run).
|
|
62
|
+
2. Right-click the element $\rightarrow$ **Plugins** $\rightarrow$ **Figma Node JSON Extractor** (or press `⌥ ⌘ P` on macOS / `Ctrl + Alt + P` on Windows to re-run).
|
|
47
63
|
3. In the plugin popup, choose your export format:
|
|
48
64
|
* **📋 Copy Node JSON**: Copies the clean AST specification directly to your clipboard (ready to paste into chat).
|
|
49
65
|
* **💾 .json**: Downloads `<name>.json` containing layout, dimensions, tokens, and props.
|
|
@@ -61,7 +77,7 @@ To install the plugin directly from your local skills folder or repository:
|
|
|
61
77
|
|
|
62
78
|
## How to Tell Your Agent to Build the UI
|
|
63
79
|
|
|
64
|
-
Once the specification is in `./.specs/`, simply instruct your agent in
|
|
80
|
+
Once the specification is in `./.specs/`, simply instruct your agent in Claude Code, Kiro, Cursor, OpenCode, or Antigravity:
|
|
65
81
|
|
|
66
82
|
```text
|
|
67
83
|
Build the component from .specs/card.json
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Figma Node JSON Extractor",
|
|
3
|
-
"id": "
|
|
3
|
+
"id": "1691244420692136789",
|
|
4
4
|
"api": "1.0.0",
|
|
5
5
|
"main": "code.js",
|
|
6
6
|
"ui": "ui.html",
|
|
7
7
|
"capabilities": [],
|
|
8
8
|
"enableProposedApi": false,
|
|
9
|
-
"editorType": ["figma", "dev"]
|
|
9
|
+
"editorType": ["figma", "dev"],
|
|
10
|
+
"documentAccess": "dynamic-page",
|
|
11
|
+
"networkAccess": {
|
|
12
|
+
"allowedDomains": [
|
|
13
|
+
"none"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
10
16
|
}
|
|
@@ -11,27 +11,138 @@ const { URL } = require('url');
|
|
|
11
11
|
// Environment and Auth Utilities
|
|
12
12
|
// -----------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function resolveHomeDir() {
|
|
15
|
+
return (
|
|
16
|
+
process.env.HOME ||
|
|
17
|
+
process.env.USERPROFILE ||
|
|
18
|
+
(typeof os.homedir === 'function' ? os.homedir() : '') ||
|
|
19
|
+
''
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function decodeEnvBuffer(buf) {
|
|
24
|
+
if (!Buffer.isBuffer(buf)) {
|
|
25
|
+
const str = String(buf || '');
|
|
26
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
27
|
+
}
|
|
28
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
|
29
|
+
return buf.toString('utf16le', 2);
|
|
30
|
+
}
|
|
31
|
+
const str = buf.toString('utf8');
|
|
32
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPowerShellProfilePaths(home) {
|
|
36
|
+
const paths = [];
|
|
37
|
+
if (process.env.PROFILE && process.env.PROFILE.toLowerCase().endsWith('.ps1')) {
|
|
38
|
+
paths.push(process.env.PROFILE);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const docRoots = [];
|
|
42
|
+
if (home) {
|
|
43
|
+
docRoots.push(
|
|
44
|
+
path.join(home, 'Documents'),
|
|
45
|
+
path.join(home, 'Documentos'),
|
|
46
|
+
path.join(home, 'OneDrive', 'Documents'),
|
|
47
|
+
path.join(home, 'OneDrive', 'Documentos'),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const oneDriveEnvs = [
|
|
52
|
+
process.env.OneDrive,
|
|
53
|
+
process.env.OneDriveConsumer,
|
|
54
|
+
process.env.OneDriveCommercial,
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
|
|
57
|
+
for (const od of oneDriveEnvs) {
|
|
58
|
+
docRoots.push(path.join(od, 'Documents'), path.join(od, 'Documentos'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const shellDirs = ['PowerShell', 'WindowsPowerShell'];
|
|
62
|
+
const profileFiles = [
|
|
63
|
+
'Microsoft.PowerShell_profile.ps1',
|
|
64
|
+
'Microsoft.VSCode_profile.ps1',
|
|
65
|
+
'profile.ps1',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const docRoot of docRoots) {
|
|
69
|
+
if (!fs.existsSync(docRoot)) continue;
|
|
70
|
+
for (const shellDir of shellDirs) {
|
|
71
|
+
const fullShellDir = path.join(docRoot, shellDir);
|
|
72
|
+
if (!fs.existsSync(fullShellDir)) continue;
|
|
73
|
+
for (const profileFile of profileFiles) {
|
|
74
|
+
paths.push(path.join(fullShellDir, profileFile));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (home) {
|
|
80
|
+
const xdgPsDir = path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'powershell');
|
|
81
|
+
if (fs.existsSync(xdgPsDir)) {
|
|
82
|
+
for (const profileFile of profileFiles) {
|
|
83
|
+
paths.push(path.join(xdgPsDir, profileFile));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Array.from(new Set(paths));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadEnv(options = {}) {
|
|
92
|
+
const cwd = options.cwd || process.cwd();
|
|
93
|
+
const home = options.home !== undefined ? options.home : resolveHomeDir();
|
|
94
|
+
|
|
95
|
+
const dotEnvCandidates = [
|
|
96
|
+
path.join(cwd, '.env'),
|
|
97
|
+
path.join(cwd, '.env.local'),
|
|
18
98
|
home ? path.join(home, '.env') : null,
|
|
99
|
+
].filter(Boolean);
|
|
100
|
+
|
|
101
|
+
const shellConfigCandidates = [
|
|
19
102
|
home ? path.join(home, '.zshrc') : null,
|
|
103
|
+
home ? path.join(home, '.zprofile') : null,
|
|
104
|
+
home ? path.join(home, '.zshenv') : null,
|
|
20
105
|
home ? path.join(home, '.bashrc') : null,
|
|
106
|
+
home ? path.join(home, '.bash_profile') : null,
|
|
107
|
+
home ? path.join(home, '.profile') : null,
|
|
108
|
+
...getPowerShellProfilePaths(home),
|
|
21
109
|
].filter(Boolean);
|
|
22
110
|
|
|
23
|
-
|
|
24
|
-
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const candidates = [];
|
|
113
|
+
for (const p of dotEnvCandidates) {
|
|
114
|
+
if (!seen.has(p)) {
|
|
115
|
+
seen.add(p);
|
|
116
|
+
candidates.push({ filePath: p, isShellConfig: false });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const p of shellConfigCandidates) {
|
|
120
|
+
if (!seen.has(p)) {
|
|
121
|
+
seen.add(p);
|
|
122
|
+
candidates.push({ filePath: p, isShellConfig: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const { filePath, isShellConfig } of candidates) {
|
|
127
|
+
if (fs.existsSync(filePath)) {
|
|
25
128
|
try {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
129
|
+
const rawBuf = fs.readFileSync(filePath);
|
|
130
|
+
const envContent = decodeEnvBuffer(rawBuf);
|
|
131
|
+
if (isShellConfig && !/figma_/i.test(envContent)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const lines = envContent.split(/\r?\n/);
|
|
28
135
|
for (const line of lines) {
|
|
136
|
+
if (isShellConfig && !/figma_/i.test(line)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
29
139
|
const parsed = parseEnvLine(line);
|
|
30
140
|
if (parsed && parsed.key) {
|
|
141
|
+
const isFigmaVar = /^FIGMA_/i.test(parsed.key);
|
|
142
|
+
const envKey = isFigmaVar ? parsed.key.toUpperCase() : parsed.key;
|
|
31
143
|
// For dot-env files, load any variable; for shell configs, only load FIGMA_ variables
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
process.env[parsed.key] = parsed.val;
|
|
144
|
+
if (!process.env[envKey] && (!isShellConfig || isFigmaVar)) {
|
|
145
|
+
process.env[envKey] = parsed.val;
|
|
35
146
|
}
|
|
36
147
|
}
|
|
37
148
|
}
|
|
@@ -44,16 +155,47 @@ function loadEnv() {
|
|
|
44
155
|
|
|
45
156
|
function parseEnvLine(line) {
|
|
46
157
|
if (!line || typeof line !== 'string') return null;
|
|
47
|
-
let trimmed = line.trim();
|
|
48
|
-
if (trimmed.startsWith('
|
|
49
|
-
|
|
158
|
+
let trimmed = line.replace(/^\uFEFF/, '').trim();
|
|
159
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle PowerShell [Environment]::SetEnvironmentVariable("KEY", "VAL", ...)
|
|
164
|
+
const setEnvMatch = trimmed.match(
|
|
165
|
+
/^\[(?:System\.)?Environment\]::SetEnvironmentVariable\(\s*(['"])([A-Za-z_][A-Za-z0-9_]*)\1\s*,\s*(['"])([^'"]*)\3/i,
|
|
166
|
+
);
|
|
167
|
+
if (setEnvMatch) {
|
|
168
|
+
return { key: setEnvMatch[2], val: setEnvMatch[4] };
|
|
50
169
|
}
|
|
51
|
-
|
|
170
|
+
|
|
171
|
+
// Strip Unix 'export ' or Windows CMD 'set ' prefix (requires whitespace after keyword)
|
|
172
|
+
if (/^export\s+/i.test(trimmed)) {
|
|
173
|
+
trimmed = trimmed.replace(/^export\s+/i, '').trim();
|
|
174
|
+
} else if (/^set\s+/i.test(trimmed)) {
|
|
175
|
+
trimmed = trimmed.replace(/^set\s+/i, '').trim();
|
|
176
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.includes('=')) {
|
|
177
|
+
trimmed = trimmed.slice(1, -1).trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!trimmed.includes('=')) {
|
|
52
182
|
return null;
|
|
53
183
|
}
|
|
54
184
|
|
|
55
185
|
const eqIdx = trimmed.indexOf('=');
|
|
56
|
-
|
|
186
|
+
let key = trimmed.slice(0, eqIdx).trim();
|
|
187
|
+
|
|
188
|
+
// Strip PowerShell $env:KEY or ${env:KEY} syntax
|
|
189
|
+
if (/^\$env:/i.test(key)) {
|
|
190
|
+
key = key.replace(/^\$env:/i, '').trim();
|
|
191
|
+
} else if (/^\$\{env:([^}]+)\}$/i.test(key)) {
|
|
192
|
+
key = key.replace(/^\$\{env:([^}]+)\}$/i, '$1').trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
57
199
|
let rest = trimmed.slice(eqIdx + 1).trim();
|
|
58
200
|
|
|
59
201
|
let val = '';
|
|
@@ -566,7 +708,7 @@ const MAX_REQUESTS_PER_WINDOW = 5; // Max 5 calls per minute (well below Figma 1
|
|
|
566
708
|
const MIN_REQUEST_INTERVAL_MS = 2500; // Minimum 2.5s between consecutive requests
|
|
567
709
|
|
|
568
710
|
function getRateGateFilePath() {
|
|
569
|
-
const home =
|
|
711
|
+
const home = resolveHomeDir();
|
|
570
712
|
return home ? path.join(home, '.figma_ratelimit.json') : path.join(process.cwd(), '.figma_ratelimit.json');
|
|
571
713
|
}
|
|
572
714
|
|
|
@@ -1132,15 +1274,24 @@ async function main() {
|
|
|
1132
1274
|
process.exit(1);
|
|
1133
1275
|
}
|
|
1134
1276
|
const { dictionary } = buildVariableMap(varsMeta);
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
config.pretty ? 2 : 0,
|
|
1142
|
-
),
|
|
1277
|
+
const tokenPayload = JSON.stringify(
|
|
1278
|
+
{
|
|
1279
|
+
tokens: dictionary,
|
|
1280
|
+
},
|
|
1281
|
+
null,
|
|
1282
|
+
config.pretty ? 2 : 0,
|
|
1143
1283
|
);
|
|
1284
|
+
if (config.specJsonPath) {
|
|
1285
|
+
const jsonDir = path.dirname(path.resolve(config.specJsonPath));
|
|
1286
|
+
if (!fs.existsSync(jsonDir)) {
|
|
1287
|
+
fs.mkdirSync(jsonDir, { recursive: true });
|
|
1288
|
+
}
|
|
1289
|
+
fs.writeFileSync(path.resolve(config.specJsonPath), tokenPayload, 'utf8');
|
|
1290
|
+
const sizeKb = (Buffer.byteLength(tokenPayload, 'utf8') / 1024).toFixed(1);
|
|
1291
|
+
console.log(`\n✔ Saved Figma design tokens to ${config.specJsonPath} (${sizeKb} KB)\n`);
|
|
1292
|
+
} else {
|
|
1293
|
+
console.log(tokenPayload);
|
|
1294
|
+
}
|
|
1144
1295
|
return;
|
|
1145
1296
|
}
|
|
1146
1297
|
|
|
@@ -1236,7 +1387,7 @@ function formatApiError(error) {
|
|
|
1236
1387
|
} else if (error.statusCode === 401 || error.statusCode === 403) {
|
|
1237
1388
|
isTokenExpiredOrInvalid = true;
|
|
1238
1389
|
errorMessage =
|
|
1239
|
-
'FIGMA_TOKEN_EXPIRED_OR_INVALID: The provided Figma access token is expired, invalid, or unauthorized. Create a new token in Figma Settings -> Personal Access Tokens and
|
|
1390
|
+
'FIGMA_TOKEN_EXPIRED_OR_INVALID: The provided Figma access token is expired, invalid, or unauthorized. Create a new token in Figma Settings -> Personal Access Tokens and set FIGMA_ACCESS_TOKEN in your environment or workspace .env file.';
|
|
1240
1391
|
} else if (error.statusCode === 429) {
|
|
1241
1392
|
isRateLimited = true;
|
|
1242
1393
|
let retryMsg = ' Please wait 30-60 seconds before making more requests.';
|
|
@@ -1284,6 +1435,9 @@ function formatApiError(error) {
|
|
|
1284
1435
|
|
|
1285
1436
|
// Export internal functions for unit testing
|
|
1286
1437
|
module.exports = {
|
|
1438
|
+
resolveHomeDir,
|
|
1439
|
+
decodeEnvBuffer,
|
|
1440
|
+
getPowerShellProfilePaths,
|
|
1287
1441
|
loadEnv,
|
|
1288
1442
|
parseEnvLine,
|
|
1289
1443
|
getFigmaToken,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
4
7
|
const {
|
|
8
|
+
decodeEnvBuffer,
|
|
5
9
|
loadEnv,
|
|
6
10
|
parseEnvLine,
|
|
7
11
|
parseFigmaUrl,
|
|
@@ -70,6 +74,88 @@ test('parseEnvLine: returns null for comments, empty lines, and lines without eq
|
|
|
70
74
|
assert.strictEqual(parseEnvLine('export FIGMA_TOKEN'), null);
|
|
71
75
|
});
|
|
72
76
|
|
|
77
|
+
test('parseEnvLine: parses Windows PowerShell $env: syntax, CRLF, and SetEnvironmentVariable', () => {
|
|
78
|
+
assert.deepStrictEqual(
|
|
79
|
+
parseEnvLine('$env:FIGMA_ACCESS_TOKEN="figd_win_ps1"\r\n'),
|
|
80
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_win_ps1' }
|
|
81
|
+
);
|
|
82
|
+
assert.deepStrictEqual(
|
|
83
|
+
parseEnvLine("$Env:FIGMA_ACCESS_TOKEN = 'figd_win_single' # ps comment\r\n"),
|
|
84
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_win_single' }
|
|
85
|
+
);
|
|
86
|
+
assert.deepStrictEqual(
|
|
87
|
+
parseEnvLine('${env:FIGMA_TOKEN}="figd_braced"'),
|
|
88
|
+
{ key: 'FIGMA_TOKEN', val: 'figd_braced' }
|
|
89
|
+
);
|
|
90
|
+
assert.deepStrictEqual(
|
|
91
|
+
parseEnvLine('[System.Environment]::SetEnvironmentVariable("FIGMA_ACCESS_TOKEN", "figd_sys_env", "User")'),
|
|
92
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_sys_env' }
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('parseEnvLine: parses Windows CMD set syntax without corrupting SETTINGS/EXPORT keys', () => {
|
|
97
|
+
assert.deepStrictEqual(
|
|
98
|
+
parseEnvLine('set FIGMA_ACCESS_TOKEN=figd_cmd_plain\r\n'),
|
|
99
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_cmd_plain' }
|
|
100
|
+
);
|
|
101
|
+
assert.deepStrictEqual(
|
|
102
|
+
parseEnvLine('set "FIGMA_ACCESS_TOKEN=figd_cmd_quoted"'),
|
|
103
|
+
{ key: 'FIGMA_ACCESS_TOKEN', val: 'figd_cmd_quoted' }
|
|
104
|
+
);
|
|
105
|
+
assert.deepStrictEqual(
|
|
106
|
+
parseEnvLine('SETTINGS=enabled'),
|
|
107
|
+
{ key: 'SETTINGS', val: 'enabled' }
|
|
108
|
+
);
|
|
109
|
+
assert.deepStrictEqual(
|
|
110
|
+
parseEnvLine('EXPORT_MODE=fast'),
|
|
111
|
+
{ key: 'EXPORT_MODE', val: 'fast' }
|
|
112
|
+
);
|
|
113
|
+
// Arbitrary PowerShell local variables must not parse as env identifiers
|
|
114
|
+
assert.strictEqual(parseEnvLine('$MaximumHistoryCount = 4096'), null);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('loadEnv & decodeEnvBuffer: loads FIGMA_ vars from UTF-16LE PowerShell profile and ignores non-FIGMA_ vars', () => {
|
|
118
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'figma-win-env-'));
|
|
119
|
+
const savedAccess = process.env.FIGMA_ACCESS_TOKEN;
|
|
120
|
+
const savedOther = process.env.UNRELATED_PS_SECRET;
|
|
121
|
+
delete process.env.FIGMA_ACCESS_TOKEN;
|
|
122
|
+
delete process.env.UNRELATED_PS_SECRET;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const psDir = path.join(tmpRoot, 'OneDrive', 'Documentos', 'WindowsPowerShell');
|
|
126
|
+
fs.mkdirSync(psDir, { recursive: true });
|
|
127
|
+
const psFile = path.join(psDir, 'Microsoft.PowerShell_profile.ps1');
|
|
128
|
+
|
|
129
|
+
const psContent = '$env:FIGMA_ACCESS_TOKEN="figd_utf16_powershell"\r\n$env:UNRELATED_PS_SECRET="should_not_leak"\r\n';
|
|
130
|
+
const utf16WithBom = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(psContent, 'utf16le')]);
|
|
131
|
+
fs.writeFileSync(psFile, utf16WithBom);
|
|
132
|
+
|
|
133
|
+
assert.ok(decodeEnvBuffer(utf16WithBom).includes('figd_utf16_powershell'));
|
|
134
|
+
|
|
135
|
+
loadEnv({ cwd: tmpRoot, home: tmpRoot });
|
|
136
|
+
assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_utf16_powershell');
|
|
137
|
+
assert.strictEqual(process.env.UNRELATED_PS_SECRET, undefined);
|
|
138
|
+
|
|
139
|
+
// Case-insensitive PowerShell $env:figma_access_token normalization
|
|
140
|
+
delete process.env.FIGMA_ACCESS_TOKEN;
|
|
141
|
+
fs.writeFileSync(psFile, '$env:figma_access_token="figd_lowercase_ps"\r\n', 'utf8');
|
|
142
|
+
loadEnv({ cwd: tmpRoot, home: tmpRoot });
|
|
143
|
+
assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_lowercase_ps');
|
|
144
|
+
|
|
145
|
+
// Workspace .env must take precedence over shell profile
|
|
146
|
+
delete process.env.FIGMA_ACCESS_TOKEN;
|
|
147
|
+
fs.writeFileSync(path.join(tmpRoot, '.env'), 'FIGMA_ACCESS_TOKEN="figd_from_dotenv"\n', 'utf8');
|
|
148
|
+
loadEnv({ cwd: tmpRoot, home: tmpRoot });
|
|
149
|
+
assert.strictEqual(process.env.FIGMA_ACCESS_TOKEN, 'figd_from_dotenv');
|
|
150
|
+
} finally {
|
|
151
|
+
if (savedAccess !== undefined) process.env.FIGMA_ACCESS_TOKEN = savedAccess;
|
|
152
|
+
else delete process.env.FIGMA_ACCESS_TOKEN;
|
|
153
|
+
if (savedOther !== undefined) process.env.UNRELATED_PS_SECRET = savedOther;
|
|
154
|
+
else delete process.env.UNRELATED_PS_SECRET;
|
|
155
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
73
159
|
// 1. URL Parsing Tests
|
|
74
160
|
test('parseFigmaUrl: extracts fileKey and nodeId with hyphens', () => {
|
|
75
161
|
const url = 'https://www.figma.com/design/1KP9pVQ1ptZzHa3Gcpr6ta/Consulta-e-pagamento?node-id=4023-474&t=abcd';
|
|
@@ -342,8 +428,8 @@ test('parseArguments: --spec sets atomic specJsonPath and companion imagePath',
|
|
|
342
428
|
const argv = ['node', 'fetch_figma.js', 'https://www.figma.com/design/xyz123/Test?node-id=1-2', '--spec', 'card'];
|
|
343
429
|
const res = parseArguments(argv);
|
|
344
430
|
assert.strictEqual(res.specName, 'card');
|
|
345
|
-
assert.strictEqual(res.specJsonPath, '.specs
|
|
346
|
-
assert.strictEqual(res.imagePath, '.specs
|
|
431
|
+
assert.strictEqual(res.specJsonPath, path.join('.specs', 'card.json'));
|
|
432
|
+
assert.strictEqual(res.imagePath, path.join('.specs', 'card.png'));
|
|
347
433
|
assert.strictEqual(res.downloadImage, false, 'Default --spec must NOT download image to conserve Tier 1 quota');
|
|
348
434
|
|
|
349
435
|
const argvWithImage = ['node', 'fetch_figma.js', 'https://www.figma.com/design/xyz123/Test?node-id=1-2', '--spec', 'card', '-i'];
|
|
@@ -353,8 +439,8 @@ test('parseArguments: --spec sets atomic specJsonPath and companion imagePath',
|
|
|
353
439
|
|
|
354
440
|
const argvCustom = ['node', 'fetch_figma.js', 'xyz123', '1:2', '--spec', 'custom/dir/navbar.json'];
|
|
355
441
|
const resCustom = parseArguments(argvCustom);
|
|
356
|
-
assert.strictEqual(resCustom.specJsonPath, 'custom
|
|
357
|
-
assert.strictEqual(resCustom.imagePath, 'custom
|
|
442
|
+
assert.strictEqual(resCustom.specJsonPath, path.join('custom', 'dir', 'navbar.json'));
|
|
443
|
+
assert.strictEqual(resCustom.imagePath, path.join('custom', 'dir', 'navbar.png'));
|
|
358
444
|
assert.strictEqual(resCustom.downloadImage, false);
|
|
359
445
|
});
|
|
360
446
|
|
|
@@ -31,7 +31,7 @@ flowchart LR
|
|
|
31
31
|
C -->|2. figma-node-builder| D["Production Component<br/>(React, Vue, Tailwind)"]
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
1. **Step 1 (Ingest)**: Design specifications are extracted into `./.specs/card.json` (and optional companion `./.specs/card.png`) using either [`figma-extractor`](../figma-extractor) (CLI / API) or the
|
|
34
|
+
1. **Step 1 (Ingest)**: Design specifications are extracted into `./.specs/card.json` (and optional companion `./.specs/card.png`) using either [`figma-extractor`](../figma-extractor) (CLI / API) or the [Figma Node JSON Extractor](https://www.figma.com/community/plugin/1691244420692136789/figma-node-json-extractor) Figma plugin.
|
|
35
35
|
2. **Review**: The human developer or architect reviews the tokens, components, and layout bounds.
|
|
36
36
|
3. **Step 2 (Build)**: **`figma-node-builder`** is invoked to build the component in your project.
|
|
37
37
|
|
|
@@ -67,7 +67,7 @@ When invoked, the agent systematically executes four phases:
|
|
|
67
67
|
|
|
68
68
|
## How to Use the Skill
|
|
69
69
|
|
|
70
|
-
Simply prompt your AI coding assistant (Claude Code, Cursor, OpenCode, Codex, or Antigravity):
|
|
70
|
+
Simply prompt your AI coding assistant (Claude Code, Kiro, Cursor, OpenCode, Codex, or Antigravity):
|
|
71
71
|
|
|
72
72
|
```text
|
|
73
73
|
"Build the component from .specs/transaction-card.json using .specs/transaction-card.png as visual reference"
|
|
@@ -39,14 +39,13 @@ Before writing any code, inspect the host project to ground your implementation:
|
|
|
39
39
|
### Phase 2: Gap Analysis & Token Mapping
|
|
40
40
|
Analyze the input JSON spec (`.specs/<name>.json`).
|
|
41
41
|
|
|
42
|
-
> **Tip**: Instead of reading the entire raw JSON AST into your context window, run the companion **Spec Inspector CLI**
|
|
42
|
+
> **Tip**: Instead of reading the entire raw JSON AST into your context window, run the companion **Spec Inspector CLI** from this skill's directory (`<skill-dir>` is the folder containing this `SKILL.md` file; if loaded from `.cursor/rules/figma-node-builder.mdc`, use `.cursor/skills/figma-node-builder`):
|
|
43
43
|
> ```bash
|
|
44
|
-
> #
|
|
45
|
-
> node
|
|
46
|
-
>
|
|
47
|
-
> node figma-node-builder/scripts/inspect_spec.js .specs/<name>.json
|
|
44
|
+
> # Compact terminal summary (always quote the script path for Windows/macOS/Linux compatibility):
|
|
45
|
+
> node "<skill-dir>/scripts/inspect_spec.js" .specs/<name>.json
|
|
46
|
+
>
|
|
48
47
|
> # Output Markdown directly for your implementation plan:
|
|
49
|
-
> node <skill-dir>/scripts/inspect_spec.js .specs/<name>.json --markdown
|
|
48
|
+
> node "<skill-dir>/scripts/inspect_spec.js" .specs/<name>.json --markdown
|
|
50
49
|
> ```
|
|
51
50
|
|
|
52
51
|
* **Component Instances**: For each node with `type: "INSTANCE"`, match `component.name` against existing codebase components. Extract its `props` (e.g., `{ variant: "outline", size: "lg", hasIcon: true }`).
|
|
@@ -54,10 +53,10 @@ Analyze the input JSON spec (`.specs/<name>.json`).
|
|
|
54
53
|
* **Gaps**: Identify any sub-components that do not exist yet in the codebase and plan to scaffold them.
|
|
55
54
|
|
|
56
55
|
### Phase 3: Grounded Code Synthesis
|
|
57
|
-
Generate component code strictly adhering to the node specification:
|
|
56
|
+
Generate component code strictly adhering to the node specification and adapting syntax to the host project's detected stack (e.g., React, Vue, Svelte, Angular, or HTML/CSS):
|
|
58
57
|
|
|
59
58
|
1. **Reusing Existing Components (Anti-Div Soup Rule)**:
|
|
60
|
-
- When an `INSTANCE` node matches an existing component, **import and render it directly
|
|
59
|
+
- When an `INSTANCE` node matches an existing component, **import and render it directly** (example in JSX — adapt to your framework):
|
|
61
60
|
```tsx
|
|
62
61
|
// Correct:
|
|
63
62
|
<Button variant="primary" size="large" hasIcon>Confirm Order</Button>
|
|
@@ -28,9 +28,21 @@ const style = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
// -----------------------------------------------------------------------------
|
|
31
|
-
// Path Resolution
|
|
31
|
+
// Path & Encoding Resolution
|
|
32
32
|
// -----------------------------------------------------------------------------
|
|
33
33
|
|
|
34
|
+
function decodeTextBuffer(buf) {
|
|
35
|
+
if (!Buffer.isBuffer(buf)) {
|
|
36
|
+
const str = String(buf || '');
|
|
37
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
38
|
+
}
|
|
39
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
|
40
|
+
return buf.toString('utf16le', 2);
|
|
41
|
+
}
|
|
42
|
+
const str = buf.toString('utf8');
|
|
43
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
function resolveSpecPath(inputPath) {
|
|
35
47
|
if (!inputPath || typeof inputPath !== 'string') {
|
|
36
48
|
throw new Error('Please specify a specification file (e.g. .specs/card.json or card)');
|
|
@@ -480,7 +492,7 @@ function main() {
|
|
|
480
492
|
|
|
481
493
|
try {
|
|
482
494
|
const resolvedPath = resolveSpecPath(positional[0]);
|
|
483
|
-
const rawContent = fs.readFileSync(resolvedPath
|
|
495
|
+
const rawContent = decodeTextBuffer(fs.readFileSync(resolvedPath));
|
|
484
496
|
const parsedData = JSON.parse(rawContent);
|
|
485
497
|
|
|
486
498
|
const inspection = inspectSpec(parsedData, resolvedPath);
|
|
@@ -507,6 +519,7 @@ if (require.main === module) {
|
|
|
507
519
|
}
|
|
508
520
|
|
|
509
521
|
module.exports = {
|
|
522
|
+
decodeTextBuffer,
|
|
510
523
|
resolveSpecPath,
|
|
511
524
|
inspectSpec,
|
|
512
525
|
formatTerminalReport,
|
|
@@ -4,7 +4,9 @@ const assert = require('assert');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
7
8
|
const {
|
|
9
|
+
decodeTextBuffer,
|
|
8
10
|
resolveSpecPath,
|
|
9
11
|
inspectSpec,
|
|
10
12
|
formatTerminalReport,
|
|
@@ -141,8 +143,21 @@ try {
|
|
|
141
143
|
} finally {
|
|
142
144
|
process.chdir(origCwd);
|
|
143
145
|
}
|
|
146
|
+
|
|
147
|
+
// 7. decodeTextBuffer & CLI UTF-8 BOM / UTF-16LE BOM resilience (Windows PowerShell 5.1 & Notepad)
|
|
148
|
+
const utf8BomFile = path.join(specsSubdir, 'utf8bom.json');
|
|
149
|
+
fs.writeFileSync(utf8BomFile, '\uFEFF' + JSON.stringify(mockSpec), 'utf8');
|
|
150
|
+
const utf16BomFile = path.join(specsSubdir, 'utf16le.json');
|
|
151
|
+
const utf16Buf = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(JSON.stringify(mockSpec), 'utf16le')]);
|
|
152
|
+
fs.writeFileSync(utf16BomFile, utf16Buf);
|
|
153
|
+
|
|
154
|
+
assert.strictEqual(JSON.parse(decodeTextBuffer(fs.readFileSync(utf8BomFile))).name, 'Product Card');
|
|
155
|
+
assert.strictEqual(JSON.parse(decodeTextBuffer(fs.readFileSync(utf16BomFile))).name, 'Product Card');
|
|
156
|
+
const cliOut = execSync(`node "${path.join(__dirname, 'inspect_spec.js')}" "${utf16BomFile}" --markdown`, { encoding: 'utf8' });
|
|
157
|
+
assert.ok(cliOut.includes('Product Card'));
|
|
158
|
+
console.log(' ✓ decodeTextBuffer & CLI: parses UTF-8 BOM and UTF-16LE BOM specs cleanly');
|
|
144
159
|
} finally {
|
|
145
160
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
146
161
|
}
|
|
147
162
|
|
|
148
|
-
console.log('\nResults:
|
|
163
|
+
console.log('\nResults: 7/7 Spec Inspector tests passed.\n');
|
package/figma-to-code/README.md
CHANGED
|
@@ -74,10 +74,16 @@ Choose one of the following terminal commands:
|
|
|
74
74
|
```bash
|
|
75
75
|
export FIGMA_ACCESS_TOKEN="figd_your_token_here"
|
|
76
76
|
```
|
|
77
|
-
- **Persist globally
|
|
77
|
+
- **Persist globally on macOS / Linux (`~/.zshrc`)**:
|
|
78
78
|
```bash
|
|
79
79
|
echo 'export FIGMA_ACCESS_TOKEN="figd_your_token_here"' >> ~/.zshrc && source ~/.zshrc
|
|
80
80
|
```
|
|
81
|
+
- **Persist globally on Windows (PowerShell)**:
|
|
82
|
+
```powershell
|
|
83
|
+
[Environment]::SetEnvironmentVariable("FIGMA_ACCESS_TOKEN", "figd_your_token_here", "User")
|
|
84
|
+
# Or inside your PowerShell profile ($PROFILE):
|
|
85
|
+
# $env:FIGMA_ACCESS_TOKEN="figd_your_token_here"
|
|
86
|
+
```
|
|
81
87
|
|
|
82
88
|
*(Optional: If behind a corporate proxy with self-signed SSL certificates, run `echo 'FIGMA_IGNORE_SSL=true' >> .env`).*
|
|
83
89
|
|
|
@@ -88,7 +94,7 @@ Flags are **completely optional** — you do not have to specify any flags:
|
|
|
88
94
|
- *"Inspect this frame and download a preview image"* (agent uses `--image` or `-i`).
|
|
89
95
|
- *"Only extract the design tokens from this file"* (agent uses `--tokens`).
|
|
90
96
|
- *"Collapse component sub-layers into props"* (agent uses `--shallow-instances`).
|
|
91
|
-
- **Tip (Guarantee Autonomous Invocation)**: To ensure the agent never skips the extractor and never estimates CSS without extracting, add this suggestion to your project's `
|
|
97
|
+
- **Tip (Guarantee Autonomous Invocation)**: To ensure the agent never skips the extractor and never estimates CSS without extracting, add this suggestion to your project's agent instructions file (e.g., `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.kiro/steering/`, or `.cursor/rules/`):
|
|
92
98
|
```markdown
|
|
93
99
|
## Figma UI Development
|
|
94
100
|
Whenever a Figma link is provided or you are asked to build UI from Figma:
|
|
@@ -142,9 +148,9 @@ node figma-to-code/scripts/fetch_figma.js "<FigmaURL>" -i --shallow-instances
|
|
|
142
148
|
|
|
143
149
|
The skill includes the following resources:
|
|
144
150
|
|
|
145
|
-
- **Agent Instruction Guide (`SKILL.md`)**: The core skill definition read by AI agents (
|
|
151
|
+
- **Agent Instruction Guide (`SKILL.md`)**: The core skill definition read by AI agents (Claude Code, Kiro, Cursor, OpenCode, Codex, Antigravity) containing workflow triggers and pair-programming guidelines.
|
|
146
152
|
- **Core Extraction Script (`scripts/fetch_figma.js`)**: The standalone, zero-dependency Node.js script that interfaces with the Figma REST API, resolves tokens, prunes the AST, and downloads preview images.
|
|
147
|
-
- **Unit Test Suite (`scripts/test_fetch_figma.js`)**:
|
|
153
|
+
- **Unit Test Suite (`scripts/test_fetch_figma.js`)**: Automated unit tests verifying environment/profile parsing, URL parsing, token formatting, component prop cleaning, rate-limit gates, and node pruning without requiring network access.
|
|
148
154
|
- **Specification Directory (`.specs/`)**: The local workspace directory where downloaded image previews are stored for multimodal visual inspection.
|
|
149
155
|
- **Reference Documentation (`README.md`)**: This guide explaining architecture, MCP comparison, and operational flags.
|
|
150
156
|
|