figma-local 2.0.0 → 2.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/package.json +1 -1
- package/skills/figma-component-audit/SKILL.md +110 -0
- package/src/component-audit.js +312 -0
- package/src/index.js +121 -0
package/package.json
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-component-audit
|
|
3
|
+
description: |
|
|
4
|
+
Use this skill when the user wants to audit, review, or check the quality of Figma components. Triggers on: "audit", "audit components", "audit all components", "check component quality", "what's wrong with my components", "component issues", "find problems in components", "review my design system components", "component health", "component score", "check for hardcoded colors", "missing descriptions", "detached instances", "incomplete variants". Requires a Figma file to be open and the daemon connected.
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Bash(fig component-audit *)
|
|
7
|
+
- Bash(fig component-audit)
|
|
8
|
+
- Bash(fig daemon status)
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Figma Component Audit
|
|
12
|
+
|
|
13
|
+
Audit Figma components for design-system quality issues. Returns a score (0–100) and a categorized list of issues per component.
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
The `fig` CLI must be connected. Check with `fig daemon status`. If not connected: `fig connect --safe`.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Audit current selection
|
|
22
|
+
|
|
23
|
+
Select a component or frame in Figma, then:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
fig component-audit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Audit a specific component by name
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
fig component-audit "Button"
|
|
33
|
+
fig component-audit "Card"
|
|
34
|
+
fig component-audit "Navigation Bar"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Audit ALL components on the current page
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
fig component-audit --all
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This is the most useful command for a full design-system review. It scans every `COMPONENT` and `COMPONENT_SET` on the page, ranks them by score (worst first), and prints a summary.
|
|
44
|
+
|
|
45
|
+
### Audit by node ID
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
fig component-audit --node "123:456"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### JSON output (for piping or saving)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
fig component-audit --all --json
|
|
55
|
+
fig component-audit --all --json > audit-report.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Include info-level issues
|
|
59
|
+
|
|
60
|
+
By default, only errors and warnings are shown. Add `--verbose` to also see info-level suggestions:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
fig component-audit --all --verbose
|
|
64
|
+
fig component-audit "Button" --verbose
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What Gets Checked
|
|
68
|
+
|
|
69
|
+
| Rule | Severity | What it flags |
|
|
70
|
+
|------|----------|---------------|
|
|
71
|
+
| `missing-description` | warning | Component has no description set |
|
|
72
|
+
| `incomplete-variants` | warning | Component set is missing expected variant combinations |
|
|
73
|
+
| `hidden-layer` | info | A child layer is hidden (dead weight in the file) |
|
|
74
|
+
| `generic-layer-name` | info | Layer has a default name like "Frame 2" or "Rectangle" |
|
|
75
|
+
| `empty-text` | warning | A text node exists but has no content |
|
|
76
|
+
| `hardcoded-color` | warning | A solid fill color with no variable binding |
|
|
77
|
+
| `no-auto-layout` | info | A frame with 2+ children but no auto layout enabled |
|
|
78
|
+
| `detached-instance` | error | An instance whose main component is missing |
|
|
79
|
+
| `deep-nesting` | info | A node nested 7+ levels deep |
|
|
80
|
+
|
|
81
|
+
## Scoring
|
|
82
|
+
|
|
83
|
+
`score = 100 − (errors × 15) − (warnings × 5) − (info × 2)`
|
|
84
|
+
|
|
85
|
+
- **≥ 80** — Good
|
|
86
|
+
- **60–79** — Fair
|
|
87
|
+
- **< 60** — Needs work
|
|
88
|
+
|
|
89
|
+
## Workflow: Full Design-System Audit
|
|
90
|
+
|
|
91
|
+
1. Open the Figma file containing your component library
|
|
92
|
+
2. Make sure `fig` is connected: `fig daemon status`
|
|
93
|
+
3. Run the full audit:
|
|
94
|
+
```bash
|
|
95
|
+
fig component-audit --all
|
|
96
|
+
```
|
|
97
|
+
4. Review the output — components sorted worst-first
|
|
98
|
+
5. For a detailed look at the worst component:
|
|
99
|
+
```bash
|
|
100
|
+
fig component-audit "ComponentName" --verbose
|
|
101
|
+
```
|
|
102
|
+
6. Fix the issues in Figma, then re-run to verify improvement
|
|
103
|
+
|
|
104
|
+
## Tips
|
|
105
|
+
|
|
106
|
+
- Run `--all --json` to save a baseline report and compare over time
|
|
107
|
+
- `detached-instance` errors (score −15 each) are the highest priority to fix
|
|
108
|
+
- `hardcoded-color` warnings usually mean a token should be created in your variable collection
|
|
109
|
+
- Use `fig var list` to see available variable collections before fixing hardcoded colors
|
|
110
|
+
- `incomplete-variants` means your component set has a property with N options but fewer than the expected NxM combinations
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* component-audit.js — Figma component audit logic
|
|
3
|
+
*
|
|
4
|
+
* Generates Figma plugin JS code that inspects components for:
|
|
5
|
+
* - Naming issues (unnamed layers, generic names)
|
|
6
|
+
* - Missing descriptions
|
|
7
|
+
* - Hardcoded colors (no variable bindings)
|
|
8
|
+
* - Missing auto layout
|
|
9
|
+
* - Hidden layers (dead weight)
|
|
10
|
+
* - Empty text nodes
|
|
11
|
+
* - Excessive nesting depth (>6 levels)
|
|
12
|
+
* - Variant completeness for component sets
|
|
13
|
+
* - Detached instances within a component
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the Figma JS code to audit a single component node by ID.
|
|
18
|
+
* @param {string} nodeId
|
|
19
|
+
*/
|
|
20
|
+
export function buildSingleAuditCode(nodeId) {
|
|
21
|
+
return `
|
|
22
|
+
(function() {
|
|
23
|
+
var node = figma.getNodeById(${JSON.stringify(nodeId)});
|
|
24
|
+
if (!node) return { error: 'Node not found: ${nodeId}' };
|
|
25
|
+
if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'FRAME') {
|
|
26
|
+
return { error: 'Node is not a COMPONENT, COMPONENT_SET, or FRAME. Got: ' + node.type };
|
|
27
|
+
}
|
|
28
|
+
return auditComponent(node);
|
|
29
|
+
${AUDIT_HELPERS}
|
|
30
|
+
})()
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the Figma JS code to audit ALL components on the current page.
|
|
36
|
+
*/
|
|
37
|
+
export function buildAllAuditCode() {
|
|
38
|
+
return `
|
|
39
|
+
(function() {
|
|
40
|
+
var page = figma.currentPage;
|
|
41
|
+
var results = [];
|
|
42
|
+
|
|
43
|
+
function collectComponents(node) {
|
|
44
|
+
if (node.type === 'COMPONENT_SET') {
|
|
45
|
+
results.push(auditComponent(node));
|
|
46
|
+
return; // children are COMPONENT variants — covered by set audit
|
|
47
|
+
}
|
|
48
|
+
if (node.type === 'COMPONENT') {
|
|
49
|
+
// Skip components that are children of a COMPONENT_SET (audited via the set)
|
|
50
|
+
if (!node.parent || node.parent.type !== 'COMPONENT_SET') {
|
|
51
|
+
results.push(auditComponent(node));
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (node.children) {
|
|
56
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
57
|
+
collectComponents(node.children[i]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
collectComponents(page);
|
|
63
|
+
return {
|
|
64
|
+
page: page.name,
|
|
65
|
+
total: results.length,
|
|
66
|
+
components: results
|
|
67
|
+
};
|
|
68
|
+
${AUDIT_HELPERS}
|
|
69
|
+
})()
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the Figma JS code to audit the current selection.
|
|
75
|
+
*/
|
|
76
|
+
export function buildSelectionAuditCode() {
|
|
77
|
+
return `
|
|
78
|
+
(function() {
|
|
79
|
+
var sel = figma.currentPage.selection;
|
|
80
|
+
if (!sel || sel.length === 0) return { error: 'Nothing selected. Select a component or frame in Figma first.' };
|
|
81
|
+
var node = sel[0];
|
|
82
|
+
if (node.type !== 'COMPONENT' && node.type !== 'COMPONENT_SET' && node.type !== 'FRAME') {
|
|
83
|
+
return { error: 'Selection is not a COMPONENT, COMPONENT_SET, or FRAME. Got: ' + node.type };
|
|
84
|
+
}
|
|
85
|
+
return auditComponent(node);
|
|
86
|
+
${AUDIT_HELPERS}
|
|
87
|
+
})()
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Shared helper code injected into every eval string.
|
|
93
|
+
// Written as a plain string so it can be appended inside the IIFE.
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
const AUDIT_HELPERS = `
|
|
96
|
+
function auditComponent(node) {
|
|
97
|
+
var issues = [];
|
|
98
|
+
var stats = { textNodes: 0, hiddenNodes: 0, instances: 0, detachedInstances: 0, maxDepth: 0 };
|
|
99
|
+
|
|
100
|
+
// ── 1. Description check ─────────────────────────────────────────────────
|
|
101
|
+
if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
|
|
102
|
+
if (!node.description || node.description.trim() === '') {
|
|
103
|
+
issues.push({ rule: 'missing-description', severity: 'warning', message: 'Component has no description' });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── 2. Variant completeness (COMPONENT_SET only) ─────────────────────────
|
|
108
|
+
if (node.type === 'COMPONENT_SET') {
|
|
109
|
+
var propDefs = node.componentPropertyDefinitions || {};
|
|
110
|
+
var propKeys = Object.keys(propDefs);
|
|
111
|
+
var variantProps = propKeys.filter(function(k) { return propDefs[k].type === 'VARIANT'; });
|
|
112
|
+
if (variantProps.length > 0) {
|
|
113
|
+
var expected = 1;
|
|
114
|
+
variantProps.forEach(function(k) {
|
|
115
|
+
expected *= (propDefs[k].variantOptions || []).length;
|
|
116
|
+
});
|
|
117
|
+
var actual = (node.children || []).length;
|
|
118
|
+
if (actual < expected) {
|
|
119
|
+
issues.push({
|
|
120
|
+
rule: 'incomplete-variants',
|
|
121
|
+
severity: 'warning',
|
|
122
|
+
message: 'Variant set has ' + actual + ' of ' + expected + ' expected combinations',
|
|
123
|
+
details: { expected: expected, actual: actual, properties: variantProps }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── 3. Deep tree walk ────────────────────────────────────────────────────
|
|
130
|
+
var GENERIC_NAMES = /^(Frame|Rectangle|Ellipse|Group|Vector|Polygon|Star|Line|Image|Component)\\s*\\d*$/i;
|
|
131
|
+
|
|
132
|
+
function walk(n, depth) {
|
|
133
|
+
if (depth > stats.maxDepth) stats.maxDepth = depth;
|
|
134
|
+
|
|
135
|
+
// Hidden layers
|
|
136
|
+
if (depth > 0 && n.visible === false) {
|
|
137
|
+
stats.hiddenNodes++;
|
|
138
|
+
issues.push({ rule: 'hidden-layer', severity: 'info', message: 'Hidden layer: "' + n.name + '"', nodeId: n.id });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Generic / unnamed layer
|
|
142
|
+
if (depth > 0 && GENERIC_NAMES.test(n.name)) {
|
|
143
|
+
issues.push({ rule: 'generic-layer-name', severity: 'info', message: 'Generic layer name: "' + n.name + '"', nodeId: n.id });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Text nodes
|
|
147
|
+
if (n.type === 'TEXT') {
|
|
148
|
+
stats.textNodes++;
|
|
149
|
+
if (!n.characters || n.characters.trim() === '') {
|
|
150
|
+
issues.push({ rule: 'empty-text', severity: 'warning', message: 'Empty text node: "' + n.name + '"', nodeId: n.id });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Hardcoded colors — fills with no variable binding
|
|
155
|
+
if (n.fills && Array.isArray(n.fills)) {
|
|
156
|
+
for (var i = 0; i < n.fills.length; i++) {
|
|
157
|
+
var fill = n.fills[i];
|
|
158
|
+
if (fill.type === 'SOLID' && fill.visible !== false) {
|
|
159
|
+
var hasBinding = n.boundVariables && n.boundVariables.fills;
|
|
160
|
+
if (!hasBinding) {
|
|
161
|
+
var r = Math.round((fill.color.r || 0) * 255);
|
|
162
|
+
var g = Math.round((fill.color.g || 0) * 255);
|
|
163
|
+
var b = Math.round((fill.color.b || 0) * 255);
|
|
164
|
+
var hex = '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
|
|
165
|
+
// Only flag non-transparent, non-white fills that look like intentional colors
|
|
166
|
+
if (hex !== '#ffffff' && hex !== '#000000' && !(r === g && g === b)) {
|
|
167
|
+
issues.push({ rule: 'hardcoded-color', severity: 'warning', message: 'Hardcoded fill color ' + hex + ' on "' + n.name + '" — consider using a variable', nodeId: n.id });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Missing auto layout on FRAME nodes that contain multiple children
|
|
175
|
+
if (n.type === 'FRAME' && depth > 0) {
|
|
176
|
+
var childCount = (n.children || []).length;
|
|
177
|
+
if (childCount >= 2 && n.layoutMode === 'NONE') {
|
|
178
|
+
issues.push({ rule: 'no-auto-layout', severity: 'info', message: 'Frame "' + n.name + '" has ' + childCount + ' children but no auto layout', nodeId: n.id });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Instances (check for detached)
|
|
183
|
+
if (n.type === 'INSTANCE') {
|
|
184
|
+
stats.instances++;
|
|
185
|
+
if (!n.mainComponent) {
|
|
186
|
+
stats.detachedInstances++;
|
|
187
|
+
issues.push({ rule: 'detached-instance', severity: 'error', message: 'Detached instance: "' + n.name + '" — main component missing', nodeId: n.id });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Excessive nesting
|
|
192
|
+
if (depth === 7) {
|
|
193
|
+
issues.push({ rule: 'deep-nesting', severity: 'info', message: 'Node "' + n.name + '" is nested 7+ levels deep', nodeId: n.id });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (n.children) {
|
|
197
|
+
for (var ci = 0; ci < n.children.length; ci++) {
|
|
198
|
+
walk(n.children[ci], depth + 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
walk(node, 0);
|
|
204
|
+
|
|
205
|
+
// ── 4. Score ─────────────────────────────────────────────────────────────
|
|
206
|
+
var errors = issues.filter(function(i) { return i.severity === 'error'; }).length;
|
|
207
|
+
var warnings = issues.filter(function(i) { return i.severity === 'warning'; }).length;
|
|
208
|
+
var infos = issues.filter(function(i) { return i.severity === 'info'; }).length;
|
|
209
|
+
var score = Math.max(0, 100 - errors * 15 - warnings * 5 - infos * 2);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
id: node.id,
|
|
213
|
+
name: node.name,
|
|
214
|
+
type: node.type,
|
|
215
|
+
score: score,
|
|
216
|
+
summary: { errors: errors, warnings: warnings, info: infos },
|
|
217
|
+
stats: stats,
|
|
218
|
+
issues: issues
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Formatter — turns raw audit result into human-readable CLI output
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Format a single component audit result for terminal output.
|
|
229
|
+
* @param {object} result - from auditComponent()
|
|
230
|
+
* @param {object} chalk - chalk instance
|
|
231
|
+
* @param {boolean} verbose - show info-level issues
|
|
232
|
+
*/
|
|
233
|
+
export function formatAuditResult(result, chalk, verbose = true) {
|
|
234
|
+
if (result.error) return chalk.red('✗ ' + result.error);
|
|
235
|
+
|
|
236
|
+
const lines = [];
|
|
237
|
+
const scoreColor = result.score >= 80 ? chalk.green : result.score >= 60 ? chalk.yellow : chalk.red;
|
|
238
|
+
const scoreLabel = result.score >= 80 ? 'Good' : result.score >= 60 ? 'Fair' : 'Needs work';
|
|
239
|
+
|
|
240
|
+
lines.push(`\n${chalk.bold(result.name)} ${chalk.gray('(' + result.type + ')')}`);
|
|
241
|
+
lines.push(
|
|
242
|
+
` Score: ${scoreColor(result.score + '/100')} ${chalk.gray('(' + scoreLabel + ')')} ` +
|
|
243
|
+
chalk.red(result.summary.errors + ' errors') + ' ' +
|
|
244
|
+
chalk.yellow(result.summary.warnings + ' warnings') + ' ' +
|
|
245
|
+
chalk.gray(result.summary.info + ' info')
|
|
246
|
+
);
|
|
247
|
+
lines.push(
|
|
248
|
+
chalk.gray(
|
|
249
|
+
` Stats: ${result.stats.textNodes} text nodes, ${result.stats.instances} instances` +
|
|
250
|
+
(result.stats.detachedInstances ? chalk.red(` (${result.stats.detachedInstances} detached)`) : '') +
|
|
251
|
+
`, ${result.stats.hiddenNodes} hidden, max depth ${result.stats.maxDepth}`
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const shown = result.issues.filter(i => verbose || i.severity !== 'info');
|
|
256
|
+
if (shown.length === 0) {
|
|
257
|
+
lines.push(chalk.green(' ✓ No issues found'));
|
|
258
|
+
} else {
|
|
259
|
+
lines.push('');
|
|
260
|
+
for (const issue of shown) {
|
|
261
|
+
const icon = issue.severity === 'error' ? chalk.red('✗') :
|
|
262
|
+
issue.severity === 'warning' ? chalk.yellow('⚠') : chalk.gray('ℹ');
|
|
263
|
+
const ruleTag = chalk.gray(`[${issue.rule}]`);
|
|
264
|
+
lines.push(` ${icon} ${issue.message} ${ruleTag}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Format the all-components audit result for terminal output.
|
|
273
|
+
* @param {object} result - { page, total, components[] }
|
|
274
|
+
* @param {object} chalk
|
|
275
|
+
* @param {boolean} verbose
|
|
276
|
+
*/
|
|
277
|
+
export function formatAllAuditResult(result, chalk, verbose = false) {
|
|
278
|
+
if (result.error) return chalk.red('✗ ' + result.error);
|
|
279
|
+
|
|
280
|
+
const lines = [];
|
|
281
|
+
const comps = result.components || [];
|
|
282
|
+
const totalErrors = comps.reduce((s, c) => s + c.summary.errors, 0);
|
|
283
|
+
const totalWarnings = comps.reduce((s, c) => s + c.summary.warnings, 0);
|
|
284
|
+
const avgScore = comps.length ? Math.round(comps.reduce((s, c) => s + c.score, 0) / comps.length) : 0;
|
|
285
|
+
|
|
286
|
+
lines.push('');
|
|
287
|
+
lines.push(chalk.bold(`Component Audit — ${result.page}`));
|
|
288
|
+
lines.push(
|
|
289
|
+
` ${comps.length} component${comps.length !== 1 ? 's' : ''} scanned ` +
|
|
290
|
+
chalk.red(totalErrors + ' errors') + ' ' +
|
|
291
|
+
chalk.yellow(totalWarnings + ' warnings') + ' ' +
|
|
292
|
+
`Avg score: ${avgScore}/100`
|
|
293
|
+
);
|
|
294
|
+
lines.push('');
|
|
295
|
+
|
|
296
|
+
// Sort: worst score first
|
|
297
|
+
const sorted = [...comps].sort((a, b) => a.score - b.score);
|
|
298
|
+
|
|
299
|
+
for (const comp of sorted) {
|
|
300
|
+
lines.push(formatAuditResult(comp, chalk, verbose));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push(chalk.gray('─'.repeat(60)));
|
|
305
|
+
lines.push(
|
|
306
|
+
`${comps.filter(c => c.score >= 80).length} good ` +
|
|
307
|
+
`${comps.filter(c => c.score >= 60 && c.score < 80).length} fair ` +
|
|
308
|
+
`${comps.filter(c => c.score < 60).length} need work`
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return lines.join('\n');
|
|
312
|
+
}
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
STAGE1_METADATA, buildFrameStructureCode, buildUsedTokensCode, formatLeanContext
|
|
21
21
|
} from './read.js';
|
|
22
22
|
import { generatePrompt } from './prompt-templates.js';
|
|
23
|
+
import {
|
|
24
|
+
buildSingleAuditCode, buildAllAuditCode, buildSelectionAuditCode,
|
|
25
|
+
formatAuditResult, formatAllAuditResult
|
|
26
|
+
} from './component-audit.js';
|
|
23
27
|
import {
|
|
24
28
|
nullDevice, killPort, getPortPid, sleepAfterStop,
|
|
25
29
|
startFigmaApp, killFigmaApp,
|
|
@@ -11199,4 +11203,121 @@ Examples:
|
|
|
11199
11203
|
}
|
|
11200
11204
|
});
|
|
11201
11205
|
|
|
11206
|
+
// ─── Component Audit ────────────────────────────────────────────────────────
|
|
11207
|
+
|
|
11208
|
+
program
|
|
11209
|
+
.command('component-audit [name]')
|
|
11210
|
+
.description('Audit components for quality issues (naming, tokens, layout, variants, detached instances)')
|
|
11211
|
+
.option('--all', 'Audit ALL components on the current page')
|
|
11212
|
+
.option('--node <nodeId>', 'Audit a specific component by node ID')
|
|
11213
|
+
.option('--json', 'Output raw JSON')
|
|
11214
|
+
.option('--verbose', 'Show info-level issues in addition to errors/warnings')
|
|
11215
|
+
.addHelpText('after', `
|
|
11216
|
+
Checks run on every component:
|
|
11217
|
+
missing-description No description set on the component
|
|
11218
|
+
incomplete-variants Component set is missing expected variant combinations
|
|
11219
|
+
hidden-layer Child layer is hidden (dead weight)
|
|
11220
|
+
generic-layer-name Layer has a default name like "Frame 2" or "Rectangle"
|
|
11221
|
+
empty-text Text node with no content
|
|
11222
|
+
hardcoded-color Solid fill with no variable binding
|
|
11223
|
+
no-auto-layout Frame with 2+ children but no auto layout
|
|
11224
|
+
detached-instance Instance whose main component is missing
|
|
11225
|
+
deep-nesting Node nested 7+ levels deep
|
|
11226
|
+
|
|
11227
|
+
Severity:
|
|
11228
|
+
error Must fix — structural or correctness problem
|
|
11229
|
+
warning Should fix — design system or quality concern
|
|
11230
|
+
info Nice to fix — best practice recommendation (shown with --verbose)
|
|
11231
|
+
|
|
11232
|
+
Examples:
|
|
11233
|
+
fig component-audit Audit current selection
|
|
11234
|
+
fig component-audit "Button" Audit the component named "Button"
|
|
11235
|
+
fig component-audit --all Audit every component on this page
|
|
11236
|
+
fig component-audit --node "123:456" Audit by node ID
|
|
11237
|
+
fig component-audit --all --json Machine-readable output
|
|
11238
|
+
fig component-audit --all --verbose Include info-level issues
|
|
11239
|
+
`)
|
|
11240
|
+
.action(async (name, options) => {
|
|
11241
|
+
await checkConnection();
|
|
11242
|
+
const spinner = ora('Running component audit...').start();
|
|
11243
|
+
|
|
11244
|
+
try {
|
|
11245
|
+
// ── Determine which audit code to run ──────────────────────────────────
|
|
11246
|
+
let code;
|
|
11247
|
+
let isAll = false;
|
|
11248
|
+
|
|
11249
|
+
if (options.all) {
|
|
11250
|
+
isAll = true;
|
|
11251
|
+
code = buildAllAuditCode();
|
|
11252
|
+
spinner.text = 'Auditing all components on page...';
|
|
11253
|
+
} else if (options.node) {
|
|
11254
|
+
code = buildSingleAuditCode(options.node);
|
|
11255
|
+
spinner.text = `Auditing node ${options.node}...`;
|
|
11256
|
+
} else if (name) {
|
|
11257
|
+
// Find component by name via metadata, then audit it
|
|
11258
|
+
spinner.text = `Looking up component "${name}"...`;
|
|
11259
|
+
const findCode = `
|
|
11260
|
+
(function() {
|
|
11261
|
+
function findByName(node, n) {
|
|
11262
|
+
if ((node.type === 'COMPONENT' || node.type === 'COMPONENT_SET' || node.type === 'FRAME') &&
|
|
11263
|
+
node.name.toLowerCase() === n.toLowerCase()) return node.id;
|
|
11264
|
+
if (node.children) {
|
|
11265
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
11266
|
+
var found = findByName(node.children[i], n);
|
|
11267
|
+
if (found) return found;
|
|
11268
|
+
}
|
|
11269
|
+
}
|
|
11270
|
+
return null;
|
|
11271
|
+
}
|
|
11272
|
+
return findByName(figma.currentPage, ${JSON.stringify(name)});
|
|
11273
|
+
})()
|
|
11274
|
+
`;
|
|
11275
|
+
const nodeId = await daemonExec('eval', { code: findCode });
|
|
11276
|
+
if (!nodeId) {
|
|
11277
|
+
spinner.fail(`No component named "${name}" found on the current page.`);
|
|
11278
|
+
process.exit(1);
|
|
11279
|
+
}
|
|
11280
|
+
code = buildSingleAuditCode(nodeId);
|
|
11281
|
+
spinner.text = `Auditing "${name}"...`;
|
|
11282
|
+
} else {
|
|
11283
|
+
// Default: audit current selection
|
|
11284
|
+
code = buildSelectionAuditCode();
|
|
11285
|
+
spinner.text = 'Auditing selection...';
|
|
11286
|
+
}
|
|
11287
|
+
|
|
11288
|
+
const result = await daemonExec('eval', { code });
|
|
11289
|
+
|
|
11290
|
+
if (result && result.error) {
|
|
11291
|
+
spinner.fail(result.error);
|
|
11292
|
+
process.exit(1);
|
|
11293
|
+
}
|
|
11294
|
+
|
|
11295
|
+
// ── Output ─────────────────────────────────────────────────────────────
|
|
11296
|
+
if (isAll) {
|
|
11297
|
+
const comps = result.components || [];
|
|
11298
|
+
spinner.succeed(
|
|
11299
|
+
`Audited ${comps.length} component${comps.length !== 1 ? 's' : ''} on page "${result.page}"`
|
|
11300
|
+
);
|
|
11301
|
+
if (options.json) {
|
|
11302
|
+
console.log(JSON.stringify(result, null, 2));
|
|
11303
|
+
} else {
|
|
11304
|
+
console.log(formatAllAuditResult(result, chalk, options.verbose || false));
|
|
11305
|
+
}
|
|
11306
|
+
} else {
|
|
11307
|
+
const score = result.score;
|
|
11308
|
+
const label = score >= 80 ? 'Good' : score >= 60 ? 'Fair' : 'Needs work';
|
|
11309
|
+
spinner.succeed(`Audited "${result.name}" — score ${score}/100 (${label})`);
|
|
11310
|
+
if (options.json) {
|
|
11311
|
+
console.log(JSON.stringify(result, null, 2));
|
|
11312
|
+
} else {
|
|
11313
|
+
console.log(formatAuditResult(result, chalk, options.verbose !== false));
|
|
11314
|
+
}
|
|
11315
|
+
}
|
|
11316
|
+
} catch (e) {
|
|
11317
|
+
spinner.fail(`Audit failed: ${e.message}`);
|
|
11318
|
+
process.exit(1);
|
|
11319
|
+
}
|
|
11320
|
+
});
|
|
11321
|
+
|
|
11202
11322
|
program.parse();
|
|
11323
|
+
|