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/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,
|
|
@@ -10581,27 +10585,34 @@ program
|
|
|
10581
10585
|
.option('--key <key>', 'Component key for importing')
|
|
10582
10586
|
.option('--name <name>', 'Filter by name (partial match)')
|
|
10583
10587
|
.option('--json', 'Output raw JSON')
|
|
10588
|
+
.option('--api', 'Use Figma REST API instead of plugin (for index)')
|
|
10589
|
+
.option('--token <token>', 'Figma personal access token (for --api)')
|
|
10590
|
+
.option('--file <url>', 'Figma file URL (for --api)')
|
|
10591
|
+
.option('--page <pageName>', 'Index only a specific page (for index)')
|
|
10584
10592
|
.addHelpText('after', `
|
|
10585
10593
|
Actions:
|
|
10586
10594
|
collections List all available library variable collections
|
|
10587
10595
|
variables List variables from a library collection (use --name to filter)
|
|
10588
10596
|
components List available library components on current page (use --name to filter)
|
|
10589
|
-
index Scan
|
|
10597
|
+
index Scan and save all components to a local index
|
|
10590
10598
|
search Search indexed libraries for components (use --name to filter)
|
|
10591
10599
|
import Import a component by key (use --key)
|
|
10592
10600
|
list List all indexed libraries
|
|
10593
10601
|
|
|
10602
|
+
Index modes:
|
|
10603
|
+
fig library index Scan current file via plugin (small files)
|
|
10604
|
+
fig library index --page "Buttons" Scan only one page (large files)
|
|
10605
|
+
fig library index --api --token "figd_..." --file "URL" Use REST API (best for large files)
|
|
10606
|
+
fig library index --api --file "URL" Use saved token
|
|
10607
|
+
|
|
10594
10608
|
Examples:
|
|
10595
10609
|
fig library collections List all library variable collections
|
|
10596
|
-
fig library variables List all library variables
|
|
10597
10610
|
fig library variables --name "color" List variables matching "color"
|
|
10598
|
-
fig library components List available library components
|
|
10599
10611
|
fig library components --name "button" Find button components
|
|
10600
|
-
fig library index
|
|
10612
|
+
fig library index --api --token "figd_..." --file "https://www.figma.com/design/ABC/..."
|
|
10601
10613
|
fig library search --name "button" Search indexed libraries for "button"
|
|
10602
10614
|
fig library list List all indexed libraries
|
|
10603
10615
|
fig library import --key "abc123..." Import a component by its key
|
|
10604
|
-
fig library import --key "abc123..." --name "MyButton" Import and rename
|
|
10605
10616
|
`)
|
|
10606
10617
|
.action(async (action, options) => {
|
|
10607
10618
|
checkConnection();
|
|
@@ -10794,13 +10805,214 @@ Examples:
|
|
|
10794
10805
|
console.log(` ${chalk.gray('id:')} ${result.id}`);
|
|
10795
10806
|
console.log(chalk.gray(' Component is now selected on the canvas.'));
|
|
10796
10807
|
} else if (action === 'index') {
|
|
10797
|
-
|
|
10798
|
-
|
|
10808
|
+
// Helper to save index data and print summary
|
|
10809
|
+
function saveIndexAndPrint(result, opts) {
|
|
10810
|
+
const libDir = join(homedir(), '.figma-local', 'libraries');
|
|
10811
|
+
mkdirSync(libDir, { recursive: true });
|
|
10812
|
+
const safeName = result.fileName.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
10813
|
+
const libFile = join(libDir, `${safeName}.json`);
|
|
10814
|
+
|
|
10815
|
+
// If file exists and we're doing page-by-page, merge
|
|
10816
|
+
let existingComponents = [];
|
|
10817
|
+
let existingPages = new Set();
|
|
10818
|
+
if (opts.page && existsSync(libFile)) {
|
|
10819
|
+
try {
|
|
10820
|
+
const existing = JSON.parse(readFileSync(libFile, 'utf8'));
|
|
10821
|
+
// Keep components from OTHER pages
|
|
10822
|
+
existingComponents = (existing.components || []).filter(c => c.page !== opts.page);
|
|
10823
|
+
existing.components.forEach(c => existingPages.add(c.page));
|
|
10824
|
+
} catch (e) { /* fresh start */ }
|
|
10825
|
+
}
|
|
10826
|
+
const mergedComponents = [...existingComponents, ...result.components];
|
|
10827
|
+
result.components.forEach(c => existingPages.add(c.page));
|
|
10828
|
+
|
|
10829
|
+
const indexData = {
|
|
10830
|
+
fileName: result.fileName,
|
|
10831
|
+
fileKey: result.fileKey || '',
|
|
10832
|
+
indexedAt: new Date().toISOString(),
|
|
10833
|
+
pageCount: opts.page ? existingPages.size : result.pageCount,
|
|
10834
|
+
componentCount: mergedComponents.length,
|
|
10835
|
+
components: mergedComponents
|
|
10836
|
+
};
|
|
10837
|
+
writeFileSync(libFile, JSON.stringify(indexData, null, 2));
|
|
10838
|
+
return { indexData, libFile };
|
|
10839
|
+
}
|
|
10840
|
+
|
|
10841
|
+
function printIndexSummary(indexData, libFile, opts) {
|
|
10842
|
+
console.log(` ${chalk.gray('Saved to:')} ${libFile}`);
|
|
10843
|
+
if (opts.json) {
|
|
10844
|
+
console.log(JSON.stringify(indexData, null, 2));
|
|
10845
|
+
} else {
|
|
10846
|
+
const sets = {};
|
|
10847
|
+
const standalone = [];
|
|
10848
|
+
for (const c of indexData.components) {
|
|
10849
|
+
if (c.componentSet) {
|
|
10850
|
+
if (!sets[c.componentSet]) sets[c.componentSet] = [];
|
|
10851
|
+
sets[c.componentSet].push(c);
|
|
10852
|
+
} else {
|
|
10853
|
+
standalone.push(c);
|
|
10854
|
+
}
|
|
10855
|
+
}
|
|
10856
|
+
if (Object.keys(sets).length > 0) {
|
|
10857
|
+
console.log(chalk.cyan('\n Component Sets:'));
|
|
10858
|
+
for (const [setName, variants] of Object.entries(sets)) {
|
|
10859
|
+
console.log(` ${chalk.white(setName)} ${chalk.gray(`(${variants.length} variant${variants.length !== 1 ? 's' : ''})`)}`);
|
|
10860
|
+
}
|
|
10861
|
+
}
|
|
10862
|
+
if (standalone.length > 0) {
|
|
10863
|
+
console.log(chalk.cyan('\n Standalone Components:'));
|
|
10864
|
+
for (const c of standalone.slice(0, 50)) {
|
|
10865
|
+
console.log(` ${chalk.white(c.name)} ${chalk.gray(`[${c.page}]`)}`);
|
|
10866
|
+
}
|
|
10867
|
+
if (standalone.length > 50) {
|
|
10868
|
+
console.log(chalk.gray(` ... and ${standalone.length - 50} more`));
|
|
10869
|
+
}
|
|
10870
|
+
}
|
|
10871
|
+
}
|
|
10872
|
+
}
|
|
10873
|
+
|
|
10874
|
+
// ---- REST API mode ----
|
|
10875
|
+
if (options.api) {
|
|
10876
|
+
// Get or save token
|
|
10877
|
+
const configDir = join(homedir(), '.figma-local');
|
|
10878
|
+
mkdirSync(configDir, { recursive: true });
|
|
10879
|
+
const tokenFile = join(configDir, 'figma-token');
|
|
10880
|
+
let token = options.token || '';
|
|
10881
|
+
if (!token && existsSync(tokenFile)) {
|
|
10882
|
+
token = readFileSync(tokenFile, 'utf8').trim();
|
|
10883
|
+
}
|
|
10884
|
+
if (!token) {
|
|
10885
|
+
spinner.fail('Figma token required. Get one from: Figma → Settings → Personal Access Tokens\nThen run: fig library index --api --token "figd_..." --file "URL"');
|
|
10886
|
+
process.exit(1);
|
|
10887
|
+
}
|
|
10888
|
+
// Save token for future use
|
|
10889
|
+
if (options.token) {
|
|
10890
|
+
writeFileSync(tokenFile, token);
|
|
10891
|
+
}
|
|
10892
|
+
|
|
10893
|
+
if (!options.file) {
|
|
10894
|
+
spinner.fail('--file is required with --api. Provide a Figma file URL.\nExample: fig library index --api --file "https://www.figma.com/design/ABC123/MyFile"');
|
|
10895
|
+
process.exit(1);
|
|
10896
|
+
}
|
|
10897
|
+
|
|
10898
|
+
// Extract file key from URL
|
|
10899
|
+
const fileKeyMatch = options.file.match(/(?:file|design)\/([a-zA-Z0-9]+)/);
|
|
10900
|
+
if (!fileKeyMatch) {
|
|
10901
|
+
spinner.fail('Could not extract file key from URL. Use a URL like: https://www.figma.com/design/ABC123/MyFile');
|
|
10902
|
+
process.exit(1);
|
|
10903
|
+
}
|
|
10904
|
+
const fileKey = fileKeyMatch[1];
|
|
10905
|
+
|
|
10906
|
+
spinner.text = `Fetching file metadata from Figma API...`;
|
|
10907
|
+
|
|
10908
|
+
// Fetch the file from the REST API
|
|
10909
|
+
const https = await import('https');
|
|
10910
|
+
function figmaApiGet(endpoint) {
|
|
10911
|
+
return new Promise((resolve, reject) => {
|
|
10912
|
+
const url = `https://api.figma.com/v1${endpoint}`;
|
|
10913
|
+
const req = https.get(url, { headers: { 'X-Figma-Token': token } }, (res) => {
|
|
10914
|
+
let data = '';
|
|
10915
|
+
res.on('data', chunk => data += chunk);
|
|
10916
|
+
res.on('end', () => {
|
|
10917
|
+
if (res.statusCode === 403) {
|
|
10918
|
+
reject(new Error('Access denied. Check your token has read access to this file.'));
|
|
10919
|
+
} else if (res.statusCode === 404) {
|
|
10920
|
+
reject(new Error('File not found. Check the URL is correct.'));
|
|
10921
|
+
} else if (res.statusCode !== 200) {
|
|
10922
|
+
reject(new Error(`Figma API returned ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
10923
|
+
} else {
|
|
10924
|
+
try { resolve(JSON.parse(data)); }
|
|
10925
|
+
catch (e) { reject(new Error('Invalid JSON response from Figma API')); }
|
|
10926
|
+
}
|
|
10927
|
+
});
|
|
10928
|
+
});
|
|
10929
|
+
req.on('error', reject);
|
|
10930
|
+
});
|
|
10931
|
+
}
|
|
10932
|
+
|
|
10799
10933
|
try {
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10934
|
+
// Get file components via the components endpoint (lightweight)
|
|
10935
|
+
spinner.text = 'Fetching components from Figma API...';
|
|
10936
|
+
const compData = await figmaApiGet(`/files/${fileKey}/components`);
|
|
10937
|
+
|
|
10938
|
+
const components = [];
|
|
10939
|
+
if (compData.meta && compData.meta.components) {
|
|
10940
|
+
for (const comp of compData.meta.components) {
|
|
10941
|
+
const c = {
|
|
10942
|
+
name: comp.name,
|
|
10943
|
+
key: comp.key,
|
|
10944
|
+
id: comp.node_id,
|
|
10945
|
+
page: comp.containing_frame ? comp.containing_frame.pageName || '' : '',
|
|
10946
|
+
description: comp.description || '',
|
|
10947
|
+
};
|
|
10948
|
+
if (comp.containing_frame && comp.containing_frame.name) {
|
|
10949
|
+
// The containing_frame for component set variants shows the set name
|
|
10950
|
+
c.componentSet = comp.containing_frame.name !== comp.name ? comp.containing_frame.name : undefined;
|
|
10951
|
+
}
|
|
10952
|
+
components.push(c);
|
|
10953
|
+
}
|
|
10954
|
+
}
|
|
10955
|
+
|
|
10956
|
+
// Also get component sets
|
|
10957
|
+
spinner.text = 'Fetching component sets from Figma API...';
|
|
10958
|
+
try {
|
|
10959
|
+
const setsData = await figmaApiGet(`/files/${fileKey}/component_sets`);
|
|
10960
|
+
if (setsData.meta && setsData.meta.component_sets) {
|
|
10961
|
+
for (const set of setsData.meta.component_sets) {
|
|
10962
|
+
// Mark components that belong to this set
|
|
10963
|
+
for (const comp of components) {
|
|
10964
|
+
if (comp.id && set.node_id && comp.componentSet === undefined) {
|
|
10965
|
+
// Check if node is child of this set by matching containing_frame
|
|
10966
|
+
}
|
|
10967
|
+
}
|
|
10968
|
+
}
|
|
10969
|
+
}
|
|
10970
|
+
} catch (e) { /* component_sets endpoint may not exist for all plans */ }
|
|
10971
|
+
|
|
10972
|
+
// Get file name
|
|
10973
|
+
spinner.text = 'Fetching file info...';
|
|
10974
|
+
let fileName = fileKey;
|
|
10975
|
+
try {
|
|
10976
|
+
const fileInfo = await figmaApiGet(`/files/${fileKey}?depth=1`);
|
|
10977
|
+
if (fileInfo.name) fileName = fileInfo.name;
|
|
10978
|
+
} catch (e) { /* use fileKey as fallback */ }
|
|
10979
|
+
|
|
10980
|
+
const result = {
|
|
10981
|
+
fileName,
|
|
10982
|
+
fileKey,
|
|
10983
|
+
pageCount: 0,
|
|
10984
|
+
componentCount: components.length,
|
|
10985
|
+
components
|
|
10986
|
+
};
|
|
10987
|
+
|
|
10988
|
+
const { indexData, libFile } = saveIndexAndPrint(result, options);
|
|
10989
|
+
spinner.succeed(`Indexed ${components.length} components from "${fileName}" via API`);
|
|
10990
|
+
printIndexSummary(indexData, libFile, options);
|
|
10991
|
+
|
|
10992
|
+
} catch (e) {
|
|
10993
|
+
spinner.fail(`Figma API error: ${e.message}`);
|
|
10994
|
+
process.exit(1);
|
|
10995
|
+
}
|
|
10996
|
+
|
|
10997
|
+
// ---- Plugin mode with --page filter ----
|
|
10998
|
+
} else if (options.page) {
|
|
10999
|
+
const pageName = options.page;
|
|
11000
|
+
spinner.text = `Scanning page "${pageName}" for components...`;
|
|
11001
|
+
const safePageName = pageName.replace(/'/g, "\\'");
|
|
11002
|
+
const code = `(function() {
|
|
11003
|
+
try {
|
|
11004
|
+
var components = [];
|
|
11005
|
+
var targetPage = null;
|
|
11006
|
+
var pages = figma.root.children;
|
|
11007
|
+
for (var p = 0; p < pages.length; p++) {
|
|
11008
|
+
if (pages[p].name === '${safePageName}' || pages[p].name.toLowerCase().indexOf('${safePageName.toLowerCase()}') !== -1) {
|
|
11009
|
+
targetPage = pages[p];
|
|
11010
|
+
break;
|
|
11011
|
+
}
|
|
11012
|
+
}
|
|
11013
|
+
if (!targetPage) {
|
|
11014
|
+
return { error: 'Page "${safePageName}" not found. Available pages: ' + pages.map(function(p) { return p.name; }).join(', ') };
|
|
11015
|
+
}
|
|
10804
11016
|
function scanNode(node, pageName) {
|
|
10805
11017
|
if (node.type === 'COMPONENT') {
|
|
10806
11018
|
var comp = {
|
|
@@ -10823,66 +11035,79 @@ Examples:
|
|
|
10823
11035
|
}
|
|
10824
11036
|
}
|
|
10825
11037
|
}
|
|
10826
|
-
scanNode(
|
|
11038
|
+
scanNode(targetPage, targetPage.name);
|
|
11039
|
+
return {
|
|
11040
|
+
fileName: figma.root.name,
|
|
11041
|
+
fileKey: figma.fileKey || '',
|
|
11042
|
+
pageCount: 1,
|
|
11043
|
+
componentCount: components.length,
|
|
11044
|
+
components: components
|
|
11045
|
+
};
|
|
11046
|
+
} catch(e) {
|
|
11047
|
+
return { error: e.message || 'Failed to scan page for components.' };
|
|
10827
11048
|
}
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
components: components
|
|
10834
|
-
};
|
|
10835
|
-
} catch(e) {
|
|
10836
|
-
return { error: e.message || 'Failed to scan file for components.' };
|
|
11049
|
+
})()`;
|
|
11050
|
+
const result = await daemonExec('eval', { code });
|
|
11051
|
+
if (result.error) {
|
|
11052
|
+
spinner.fail(result.error);
|
|
11053
|
+
process.exit(1);
|
|
10837
11054
|
}
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
}
|
|
10844
|
-
// Save to ~/.figma-local/libraries/
|
|
10845
|
-
const libDir = join(homedir(), '.figma-local', 'libraries');
|
|
10846
|
-
mkdirSync(libDir, { recursive: true });
|
|
10847
|
-
const safeName = result.fileName.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
|
10848
|
-
const libFile = join(libDir, `${safeName}.json`);
|
|
10849
|
-
const indexData = {
|
|
10850
|
-
fileName: result.fileName,
|
|
10851
|
-
fileKey: result.fileKey,
|
|
10852
|
-
indexedAt: new Date().toISOString(),
|
|
10853
|
-
pageCount: result.pageCount,
|
|
10854
|
-
componentCount: result.componentCount,
|
|
10855
|
-
components: result.components
|
|
10856
|
-
};
|
|
10857
|
-
writeFileSync(libFile, JSON.stringify(indexData, null, 2));
|
|
10858
|
-
spinner.succeed(`Indexed ${result.componentCount} components from "${result.fileName}" (${result.pageCount} pages)`);
|
|
10859
|
-
console.log(` ${chalk.gray('Saved to:')} ${libFile}`);
|
|
10860
|
-
if (options.json) {
|
|
10861
|
-
console.log(JSON.stringify(indexData, null, 2));
|
|
11055
|
+
const { indexData, libFile } = saveIndexAndPrint(result, { page: pageName });
|
|
11056
|
+
spinner.succeed(`Indexed ${result.componentCount} components from page "${pageName}" (${indexData.componentCount} total in library)`);
|
|
11057
|
+
printIndexSummary(indexData, libFile, options);
|
|
11058
|
+
|
|
11059
|
+
// ---- Plugin mode full scan ----
|
|
10862
11060
|
} else {
|
|
10863
|
-
|
|
10864
|
-
const
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
11061
|
+
spinner.text = 'Scanning all pages for components...';
|
|
11062
|
+
const code = `(function() {
|
|
11063
|
+
try {
|
|
11064
|
+
var components = [];
|
|
11065
|
+
var pages = figma.root.children;
|
|
11066
|
+
for (var p = 0; p < pages.length; p++) {
|
|
11067
|
+
var page = pages[p];
|
|
11068
|
+
function scanNode(node, pageName) {
|
|
11069
|
+
if (node.type === 'COMPONENT') {
|
|
11070
|
+
var comp = {
|
|
11071
|
+
name: node.name,
|
|
11072
|
+
key: node.key,
|
|
11073
|
+
id: node.id,
|
|
11074
|
+
page: pageName,
|
|
11075
|
+
description: node.description || '',
|
|
11076
|
+
w: Math.round(node.width),
|
|
11077
|
+
h: Math.round(node.height)
|
|
11078
|
+
};
|
|
11079
|
+
if (node.parent && node.parent.type === 'COMPONENT_SET') {
|
|
11080
|
+
comp.componentSet = node.parent.name;
|
|
11081
|
+
}
|
|
11082
|
+
components.push(comp);
|
|
11083
|
+
}
|
|
11084
|
+
if ('children' in node) {
|
|
11085
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
11086
|
+
scanNode(node.children[i], pageName);
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11089
|
+
}
|
|
11090
|
+
scanNode(page, page.name);
|
|
11091
|
+
}
|
|
11092
|
+
return {
|
|
11093
|
+
fileName: figma.root.name,
|
|
11094
|
+
fileKey: figma.fileKey || '',
|
|
11095
|
+
pageCount: pages.length,
|
|
11096
|
+
componentCount: components.length,
|
|
11097
|
+
components: components
|
|
11098
|
+
};
|
|
11099
|
+
} catch(e) {
|
|
11100
|
+
return { error: e.message || 'Failed to scan file for components.' };
|
|
10884
11101
|
}
|
|
11102
|
+
})()`;
|
|
11103
|
+
const result = await daemonExec('eval', { code });
|
|
11104
|
+
if (result.error) {
|
|
11105
|
+
spinner.fail(result.error);
|
|
11106
|
+
process.exit(1);
|
|
10885
11107
|
}
|
|
11108
|
+
const { indexData, libFile } = saveIndexAndPrint(result, {});
|
|
11109
|
+
spinner.succeed(`Indexed ${result.componentCount} components from "${result.fileName}" (${result.pageCount} pages)`);
|
|
11110
|
+
printIndexSummary(indexData, libFile, options);
|
|
10886
11111
|
}
|
|
10887
11112
|
} else if (action === 'search') {
|
|
10888
11113
|
const nameFilter = options.name ? options.name.toLowerCase() : '';
|
|
@@ -10978,4 +11203,121 @@ Examples:
|
|
|
10978
11203
|
}
|
|
10979
11204
|
});
|
|
10980
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
|
+
|
|
10981
11322
|
program.parse();
|
|
11323
|
+
|