figma-local 1.9.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/skills/figma-library/SKILL.md +63 -80
- package/src/component-audit.js +312 -0
- package/src/index.js +408 -66
- package/src/.figma-client-1774014578401.9038.mjs +0 -4198
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
|
|
@@ -11,162 +11,145 @@ allowed-tools:
|
|
|
11
11
|
|
|
12
12
|
# Figma Library
|
|
13
13
|
|
|
14
|
-
Access team library components and variables from other Figma files. Index libraries, search components by name, import by key, and browse design tokens.
|
|
14
|
+
Access team library components and variables from other Figma files. Index libraries via REST API or plugin, search components by name, import by key, and browse design tokens.
|
|
15
15
|
|
|
16
16
|
## Prerequisites
|
|
17
17
|
|
|
18
18
|
- The `fig` CLI must be connected: `fig daemon status`. If not: `fig connect --safe`.
|
|
19
19
|
- The Figma plugin must have `teamlibrary` permission in its manifest. If library commands fail with a permission error, re-import the plugin in Figma (Plugins → Development → Import from manifest).
|
|
20
|
-
- Libraries must be enabled in the current Figma file (check Assets panel → team library icon).
|
|
21
20
|
|
|
22
21
|
## IMPORTANT: How to access library components
|
|
23
22
|
|
|
24
|
-
Figma's plugin API does **not** allow browsing library components directly. To work with library components, you must **index** them first:
|
|
23
|
+
Figma's plugin API does **not** allow browsing library components directly. To work with library components, you must **index** them first. There are three ways to index:
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
2. Run `fig library index` — this scans all pages and saves every component with its key
|
|
28
|
-
3. Switch to your **working file**
|
|
29
|
-
4. Run `fig library search --name "button"` — finds components from indexed libraries
|
|
30
|
-
5. Run `fig library import --key "<key>"` — imports the component
|
|
25
|
+
### Option A: REST API (recommended for large files)
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
No plugin needed. Works on any file size without hanging Figma.
|
|
33
28
|
|
|
34
|
-
|
|
29
|
+
**First time — provide token and file URL:**
|
|
30
|
+
```bash
|
|
31
|
+
fig library index --api --token "figd_xxxxx" --file "https://www.figma.com/design/ABC123/MyFile"
|
|
32
|
+
```
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
The token is saved for future use. Get one from: Figma → Settings → Personal Access Tokens.
|
|
37
35
|
|
|
36
|
+
**After first time — just provide the file URL:**
|
|
38
37
|
```bash
|
|
39
|
-
fig library index
|
|
38
|
+
fig library index --api --file "https://www.figma.com/design/ABC123/MyFile"
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
### Option B: Page-by-page (for large files via plugin)
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
### Search indexed libraries
|
|
47
|
-
|
|
48
|
-
Search across all indexed libraries by component name:
|
|
43
|
+
Open the library file in Figma, then scan one page at a time:
|
|
49
44
|
|
|
50
45
|
```bash
|
|
51
|
-
fig library
|
|
52
|
-
fig library
|
|
53
|
-
fig library
|
|
54
|
-
fig library search --name "checkbox"
|
|
46
|
+
fig library index --page "Buttons"
|
|
47
|
+
fig library index --page "Inputs"
|
|
48
|
+
fig library index --page "Cards"
|
|
55
49
|
```
|
|
56
50
|
|
|
57
|
-
|
|
51
|
+
Each page's components are merged into the same index file. This avoids hanging on large files.
|
|
52
|
+
|
|
53
|
+
### Option C: Full scan (small files only)
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
Open the library file in Figma, then:
|
|
60
56
|
|
|
61
57
|
```bash
|
|
62
|
-
fig library
|
|
58
|
+
fig library index
|
|
63
59
|
```
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
**Warning:** This scans ALL pages at once. Only use on small files — large design systems will cause Figma to hang.
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
## After indexing: Search and Import
|
|
64
|
+
|
|
65
|
+
### Search indexed libraries
|
|
68
66
|
|
|
69
67
|
```bash
|
|
70
|
-
fig library
|
|
68
|
+
fig library search --name "button"
|
|
69
|
+
fig library search --name "input"
|
|
70
|
+
fig library search --name "card"
|
|
71
|
+
fig library search --name "checkbox"
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
Returns:
|
|
74
|
-
|
|
75
|
-
### Import a component by key
|
|
74
|
+
Returns: component name, key (for importing), component set, page, and library name.
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
### List indexed libraries
|
|
78
77
|
|
|
79
78
|
```bash
|
|
80
|
-
fig library
|
|
79
|
+
fig library list
|
|
81
80
|
```
|
|
82
81
|
|
|
83
|
-
Import
|
|
82
|
+
### Import by key
|
|
84
83
|
|
|
85
84
|
```bash
|
|
86
|
-
fig library import --key "
|
|
85
|
+
fig library import --key "<key-from-search>"
|
|
86
|
+
fig library import --key "<key>" --name "PrimaryButton"
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
The
|
|
89
|
+
The component is placed at viewport center and selected.
|
|
90
90
|
|
|
91
|
-
###
|
|
92
|
-
|
|
93
|
-
See what variable collections are available from linked libraries:
|
|
91
|
+
### Inspect the imported component
|
|
94
92
|
|
|
95
93
|
```bash
|
|
96
|
-
fig
|
|
94
|
+
fig inspect --deep
|
|
97
95
|
```
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
## Variables (no indexing needed)
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
Library variables are available directly via the plugin API:
|
|
102
100
|
|
|
103
101
|
```bash
|
|
104
|
-
fig library
|
|
105
|
-
fig library variables
|
|
106
|
-
fig library variables --name "
|
|
102
|
+
fig library collections # List variable collections
|
|
103
|
+
fig library variables # List all variables
|
|
104
|
+
fig library variables --name "color" # Search by name
|
|
107
105
|
```
|
|
108
106
|
|
|
109
|
-
|
|
107
|
+
## Components on current page
|
|
110
108
|
|
|
111
|
-
Find library components
|
|
109
|
+
Find library components already dragged onto the current page:
|
|
112
110
|
|
|
113
111
|
```bash
|
|
114
112
|
fig library components
|
|
115
113
|
fig library components --name "button"
|
|
116
114
|
```
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
All commands support `--json` for structured output.
|
|
116
|
+
## Full workflow
|
|
121
117
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
1. **Index** the library (one-time, in the library file):
|
|
118
|
+
1. **Index** the library (pick one method):
|
|
125
119
|
```bash
|
|
126
|
-
|
|
120
|
+
# Best for large files:
|
|
121
|
+
fig library index --api --token "figd_..." --file "https://..."
|
|
122
|
+
# Or page by page:
|
|
123
|
+
fig library index --page "Buttons"
|
|
127
124
|
```
|
|
128
125
|
|
|
129
|
-
2. **
|
|
130
|
-
|
|
131
|
-
3. **Search** for the components you need:
|
|
126
|
+
2. **Search** for components:
|
|
132
127
|
```bash
|
|
133
|
-
fig library search --name "button"
|
|
134
|
-
fig library search --name "input"
|
|
128
|
+
fig library search --name "button" --json
|
|
135
129
|
```
|
|
136
130
|
|
|
137
|
-
|
|
131
|
+
3. **Import** into your working file:
|
|
138
132
|
```bash
|
|
139
|
-
fig library import --key "<key
|
|
133
|
+
fig library import --key "<key>"
|
|
140
134
|
```
|
|
141
135
|
|
|
142
|
-
|
|
136
|
+
4. **Inspect** for full specs:
|
|
143
137
|
```bash
|
|
144
138
|
fig inspect --deep
|
|
145
139
|
```
|
|
146
140
|
|
|
147
|
-
|
|
141
|
+
5. **Get variables** for tokens:
|
|
148
142
|
```bash
|
|
149
143
|
fig library variables --name "primary" --json
|
|
150
144
|
```
|
|
151
145
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
## Workflow: Extracting a full design system
|
|
155
|
-
|
|
156
|
-
1. Open the design system file → `fig library index`
|
|
157
|
-
2. Get all components: `fig library search --name "" --json`
|
|
158
|
-
3. Get all variables: `fig library variables --json`
|
|
159
|
-
4. Import key components one by one and document them:
|
|
160
|
-
```bash
|
|
161
|
-
fig library import --key "<key>"
|
|
162
|
-
fig document --json
|
|
163
|
-
```
|
|
146
|
+
6. **Replicate** in code.
|
|
164
147
|
|
|
165
148
|
## Tips
|
|
166
149
|
|
|
167
|
-
- **
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
150
|
+
- **REST API is fastest** for large design systems — no file opening needed.
|
|
151
|
+
- Token is saved after first use in `~/.figma-local/figma-token`.
|
|
152
|
+
- Page-by-page indexing merges results — run multiple times to build up the index.
|
|
153
|
+
- Re-index when the library is updated to get new components.
|
|
154
|
+
- `fig library search --name "" --json` returns ALL indexed components.
|
|
171
155
|
- Component keys are stable across file versions — save them for repeated imports.
|
|
172
|
-
- Use `fig library search --name "button" --json | jq '.[].key'` to extract just the keys.
|
|
@@ -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
|
+
}
|