canicode 0.3.1 → 0.3.2
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/README.md +27 -8
- package/dist/cli/index.js +80 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +67 -3
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +273 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/logo.png" alt="CanICode" width="80">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">CanICode</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/canicode"><img src="https://img.shields.io/npm/v/canicode.svg" alt="npm version"></a>
|
|
9
|
+
<a href="https://github.com/let-sunny/canicode/actions/workflows/ci.yml"><img src="https://github.com/let-sunny/canicode/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
10
|
+
<a href="https://github.com/let-sunny/canicode/actions/workflows/release.yml"><img src="https://github.com/let-sunny/canicode/actions/workflows/release.yml/badge.svg" alt="Release"></a>
|
|
11
|
+
<a href="https://let-sunny.github.io/canicode/"><img src="https://img.shields.io/badge/Try_it-GitHub_Pages-blue" alt="GitHub Pages"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">Analyze Figma designs. Score how dev-friendly and AI-friendly they are. Get actionable issues before writing code.</p>
|
|
15
|
+
|
|
16
|
+
<p align="center"><strong><a href="https://let-sunny.github.io/canicode/">Try it in your browser</a></strong> — no install needed.</p>
|
|
4
17
|
|
|
5
18
|
```bash
|
|
6
19
|
npm install -g canicode
|
|
@@ -27,13 +40,17 @@ canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
|
27
40
|
|
|
28
41
|
Each issue is classified: **Blocking** > **Risk** > **Missing Info** > **Suggestion**.
|
|
29
42
|
|
|
30
|
-
Scores use density + diversity weighting per category, combined into an overall grade (A/B/C/D/F).
|
|
43
|
+
Scores use density + diversity weighting per category, combined into an overall grade (S/A+/A/B+/B/C+/C/D/F).
|
|
31
44
|
|
|
32
45
|
---
|
|
33
46
|
|
|
34
47
|
## Getting Started
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
Four ways to use CanICode. Pick one.
|
|
50
|
+
|
|
51
|
+
### Web (no install)
|
|
52
|
+
|
|
53
|
+
Go to **[let-sunny.github.io/canicode](https://let-sunny.github.io/canicode/)**, paste a Figma URL, and get results instantly in your browser.
|
|
37
54
|
|
|
38
55
|
### CLI (standalone)
|
|
39
56
|
|
|
@@ -50,6 +67,8 @@ To enable "Comment on Figma" buttons in reports, set your Figma token:
|
|
|
50
67
|
canicode init --token figd_xxxxxxxxxxxxx
|
|
51
68
|
```
|
|
52
69
|
|
|
70
|
+
> **Get your token:** Figma → Settings → Security → Personal access tokens → Generate new token
|
|
71
|
+
|
|
53
72
|
### MCP Server (Claude Code / Cursor / Claude Desktop)
|
|
54
73
|
|
|
55
74
|
**Claude Code:**
|
|
@@ -156,7 +175,7 @@ See [`examples/custom-rules.json`](examples/custom-rules.json) | Run `canicode d
|
|
|
156
175
|
<details>
|
|
157
176
|
<summary><strong>Config Overrides</strong></summary>
|
|
158
177
|
|
|
159
|
-
Override
|
|
178
|
+
Override rule scores, severity, node exclusions, and global settings:
|
|
160
179
|
|
|
161
180
|
```bash
|
|
162
181
|
canicode analyze <url> --config ./my-config.json
|
|
@@ -164,8 +183,8 @@ canicode analyze <url> --config ./my-config.json
|
|
|
164
183
|
|
|
165
184
|
```json
|
|
166
185
|
{
|
|
186
|
+
"excludeNodeNames": ["chatbot", "ad-banner", "wip"],
|
|
167
187
|
"gridBase": 4,
|
|
168
|
-
"colorTolerance": 5,
|
|
169
188
|
"rules": {
|
|
170
189
|
"no-auto-layout": { "score": -15, "severity": "blocking" },
|
|
171
190
|
"default-name": { "enabled": false }
|
|
@@ -183,7 +202,7 @@ canicode analyze <url> --config ./my-config.json
|
|
|
183
202
|
| `rules.<id>.severity` | Override rule severity |
|
|
184
203
|
| `rules.<id>.enabled` | Enable/disable a rule |
|
|
185
204
|
|
|
186
|
-
See [`examples/config.json`](examples/config.json) | Run `canicode docs config`
|
|
205
|
+
See [`examples/config.json`](examples/config.json) | [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md) | Run `canicode docs config`
|
|
187
206
|
|
|
188
207
|
</details>
|
|
189
208
|
|
|
@@ -197,7 +216,7 @@ Density Score = 100 - (weighted issue count / node count) × 100
|
|
|
197
216
|
Diversity Score = (1 - unique violated rules / total rules in category) × 100
|
|
198
217
|
```
|
|
199
218
|
|
|
200
|
-
Severity weights issues — a single blocking issue counts 3x more than a suggestion. Scores are calculated per category and combined into an overall grade (A/B/C/D/F).
|
|
219
|
+
Severity weights issues — a single blocking issue counts 3x more than a suggestion. Scores are calculated per category and combined into an overall grade (S/A+/A/B+/B/C+/C/D/F).
|
|
201
220
|
|
|
202
221
|
</details>
|
|
203
222
|
|
package/dist/cli/index.js
CHANGED
|
@@ -502,11 +502,15 @@ var RuleEngine = class {
|
|
|
502
502
|
enabledRuleIds;
|
|
503
503
|
disabledRuleIds;
|
|
504
504
|
targetNodeId;
|
|
505
|
+
excludeNamePattern;
|
|
506
|
+
excludeNodeTypes;
|
|
505
507
|
constructor(options = {}) {
|
|
506
508
|
this.configs = options.configs ?? RULE_CONFIGS;
|
|
507
509
|
this.enabledRuleIds = options.enabledRules ? new Set(options.enabledRules) : null;
|
|
508
510
|
this.disabledRuleIds = new Set(options.disabledRules ?? []);
|
|
509
511
|
this.targetNodeId = options.targetNodeId;
|
|
512
|
+
this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
|
|
513
|
+
this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
|
|
510
514
|
}
|
|
511
515
|
/**
|
|
512
516
|
* Analyze a Figma file and return issues
|
|
@@ -560,6 +564,12 @@ var RuleEngine = class {
|
|
|
560
564
|
*/
|
|
561
565
|
traverseAndCheck(node, file, rules, maxDepth, issues, depth, path, parent, siblings) {
|
|
562
566
|
const nodePath = [...path, node.name];
|
|
567
|
+
if (this.excludeNodeTypes && this.excludeNodeTypes.has(node.type)) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
563
573
|
const context = {
|
|
564
574
|
file,
|
|
565
575
|
parent,
|
|
@@ -1479,7 +1489,7 @@ function generateHtmlReport(file, result, scores, options) {
|
|
|
1479
1489
|
${CATEGORIES.map((cat) => {
|
|
1480
1490
|
const cs = scores.byCategory[cat];
|
|
1481
1491
|
const desc = CATEGORY_DESCRIPTIONS[cat];
|
|
1482
|
-
return ` <
|
|
1492
|
+
return ` <a href="#cat-${cat}" class="flex flex-col items-center group relative cursor-pointer no-underline text-foreground hover:opacity-80 transition-opacity">
|
|
1483
1493
|
${renderGaugeSvg(cs.percentage, 100, 7)}
|
|
1484
1494
|
<span class="text-xs font-medium mt-2.5 text-center leading-tight">${CATEGORY_LABELS[cat]}</span>
|
|
1485
1495
|
<span class="text-[11px] text-muted-foreground">${cs.issueCount} issues</span>
|
|
@@ -1487,7 +1497,7 @@ ${CATEGORIES.map((cat) => {
|
|
|
1487
1497
|
${esc(desc)}
|
|
1488
1498
|
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div>
|
|
1489
1499
|
</div>
|
|
1490
|
-
</
|
|
1500
|
+
</a>`;
|
|
1491
1501
|
}).join("\n")}
|
|
1492
1502
|
</div>
|
|
1493
1503
|
</section>
|
|
@@ -1625,7 +1635,7 @@ function renderCategory(cat, scores, issues, fileKey, screenshotMap, figmaToken)
|
|
|
1625
1635
|
for (const sev of SEVERITY_ORDER) bySeverity.set(sev, []);
|
|
1626
1636
|
for (const issue of issues) bySeverity.get(issue.config.severity)?.push(issue);
|
|
1627
1637
|
return `
|
|
1628
|
-
<details class="bg-card border border-border rounded-lg shadow-sm overflow-hidden group"${hasProblems ? " open" : ""}>
|
|
1638
|
+
<details id="cat-${cat}" class="bg-card border border-border rounded-lg shadow-sm overflow-hidden group"${hasProblems ? " open" : ""}>
|
|
1629
1639
|
<summary class="px-5 py-3.5 flex items-center gap-3 cursor-pointer hover:bg-muted/50 transition-colors select-none">
|
|
1630
1640
|
<span class="inline-flex items-center justify-center w-10 h-6 rounded-md text-xs font-bold border ${scoreBadgeStyle(cs.percentage)}">${cs.percentage}</span>
|
|
1631
1641
|
<div class="flex-1 min-w-0">
|
|
@@ -2360,14 +2370,14 @@ var ELIGIBLE_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2360
2370
|
"COMPONENT",
|
|
2361
2371
|
"INSTANCE"
|
|
2362
2372
|
]);
|
|
2363
|
-
var
|
|
2373
|
+
var EXCLUDED_NAME_PATTERN = /\b(icon|ico|badge|indicator|image|asset|chatbot|cta|gnb|navigation|nav|fab|modal|dialog|popup|overlay|toast|snackbar|tooltip|dropdown|menu|sticky|bg|background|divider|separator|logo|avatar|thumbnail|thumb|header|footer|sidebar|toolbar|tabbar|tab-bar|statusbar|status-bar|spinner|loader|cursor|dot|dim|dimmed|filter)\b/i;
|
|
2364
2374
|
function filterConversionCandidates(summaries, documentRoot) {
|
|
2365
2375
|
return summaries.filter((summary) => {
|
|
2366
2376
|
const node = findNode(documentRoot, summary.nodeId);
|
|
2367
2377
|
if (!node) return false;
|
|
2368
2378
|
if (EXCLUDED_NODE_TYPES.has(node.type)) return false;
|
|
2369
2379
|
if (!ELIGIBLE_NODE_TYPES.has(node.type)) return false;
|
|
2370
|
-
if (
|
|
2380
|
+
if (EXCLUDED_NAME_PATTERN.test(node.name)) return false;
|
|
2371
2381
|
const bbox = node.absoluteBoundingBox;
|
|
2372
2382
|
if (bbox && (bbox.width < MIN_WIDTH || bbox.height < MIN_HEIGHT)) return false;
|
|
2373
2383
|
if (!node.children || node.children.length < 3) return false;
|
|
@@ -2770,7 +2780,9 @@ EXAMPLE
|
|
|
2770
2780
|
|
|
2771
2781
|
USAGE
|
|
2772
2782
|
canicode analyze <url> --config ./my-config.json
|
|
2773
|
-
|
|
2783
|
+
|
|
2784
|
+
Full guide: docs/CUSTOMIZATION.md
|
|
2785
|
+
Examples: examples/config.json
|
|
2774
2786
|
`.trimStart());
|
|
2775
2787
|
}
|
|
2776
2788
|
var DOCS_TOPICS = {
|
|
@@ -4040,9 +4052,13 @@ Warning: Analyzing ${totalNodes} nodes without scope. Results may be noisy.`);
|
|
|
4040
4052
|
Analyzing: ${file.name}`);
|
|
4041
4053
|
console.log(`Nodes: ${totalNodes}`);
|
|
4042
4054
|
let configs = options.preset ? { ...getConfigsWithPreset(options.preset) } : { ...RULE_CONFIGS };
|
|
4055
|
+
let excludeNodeNames;
|
|
4056
|
+
let excludeNodeTypes;
|
|
4043
4057
|
if (options.config) {
|
|
4044
4058
|
const configFile = await loadConfigFile(options.config);
|
|
4045
4059
|
configs = mergeConfigs(configs, configFile);
|
|
4060
|
+
excludeNodeNames = configFile.excludeNodeNames;
|
|
4061
|
+
excludeNodeTypes = configFile.excludeNodeTypes;
|
|
4046
4062
|
console.log(`Config loaded: ${options.config}`);
|
|
4047
4063
|
}
|
|
4048
4064
|
if (options.customRules) {
|
|
@@ -4055,7 +4071,9 @@ Analyzing: ${file.name}`);
|
|
|
4055
4071
|
}
|
|
4056
4072
|
const analyzeOptions = {
|
|
4057
4073
|
configs,
|
|
4058
|
-
...effectiveNodeId && { targetNodeId: effectiveNodeId }
|
|
4074
|
+
...effectiveNodeId && { targetNodeId: effectiveNodeId },
|
|
4075
|
+
...excludeNodeNames && { excludeNodeNames },
|
|
4076
|
+
...excludeNodeTypes && { excludeNodeTypes }
|
|
4059
4077
|
};
|
|
4060
4078
|
const result = analyzeFile(file, analyzeOptions);
|
|
4061
4079
|
console.log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
|
|
@@ -4346,6 +4364,61 @@ cli.command("init", "Set up canicode (Figma token or MCP)").option("--token <tok
|
|
|
4346
4364
|
process.exit(1);
|
|
4347
4365
|
}
|
|
4348
4366
|
});
|
|
4367
|
+
cli.command("list-rules", "List all analysis rules with scores and severity").option("--custom-rules <path>", "Include custom rules from JSON file").option("--config <path>", "Apply config overrides to show effective scores").option("--json", "Output as JSON").action(async (options) => {
|
|
4368
|
+
try {
|
|
4369
|
+
let configs = { ...RULE_CONFIGS };
|
|
4370
|
+
if (options.config) {
|
|
4371
|
+
const configFile = await loadConfigFile(options.config);
|
|
4372
|
+
configs = mergeConfigs(configs, configFile);
|
|
4373
|
+
}
|
|
4374
|
+
if (options.customRules) {
|
|
4375
|
+
const { rules: rules2, configs: customConfigs } = await loadCustomRules(options.customRules);
|
|
4376
|
+
for (const rule of rules2) {
|
|
4377
|
+
ruleRegistry.register(rule);
|
|
4378
|
+
}
|
|
4379
|
+
configs = { ...configs, ...customConfigs };
|
|
4380
|
+
}
|
|
4381
|
+
const rules = ruleRegistry.getAll().map((rule) => {
|
|
4382
|
+
const config2 = configs[rule.definition.id];
|
|
4383
|
+
return {
|
|
4384
|
+
id: rule.definition.id,
|
|
4385
|
+
name: rule.definition.name,
|
|
4386
|
+
category: rule.definition.category,
|
|
4387
|
+
severity: config2?.severity ?? "risk",
|
|
4388
|
+
score: config2?.score ?? 0,
|
|
4389
|
+
enabled: config2?.enabled ?? true
|
|
4390
|
+
};
|
|
4391
|
+
});
|
|
4392
|
+
if (options.json) {
|
|
4393
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
4397
|
+
for (const rule of rules) {
|
|
4398
|
+
const list = byCategory.get(rule.category) ?? [];
|
|
4399
|
+
list.push(rule);
|
|
4400
|
+
byCategory.set(rule.category, list);
|
|
4401
|
+
}
|
|
4402
|
+
for (const [category, catRules] of byCategory) {
|
|
4403
|
+
console.log(`
|
|
4404
|
+
${category.toUpperCase()}`);
|
|
4405
|
+
for (const r of catRules) {
|
|
4406
|
+
const status = r.enabled ? "" : " (disabled)";
|
|
4407
|
+
const pad = " ".repeat(Math.max(0, 40 - r.id.length));
|
|
4408
|
+
console.log(` ${r.id}${pad} ${String(r.score).padStart(4)} ${r.severity}${status}`);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
console.log(`
|
|
4412
|
+
Total: ${rules.length} rules
|
|
4413
|
+
`);
|
|
4414
|
+
} catch (error) {
|
|
4415
|
+
console.error(
|
|
4416
|
+
"\nError:",
|
|
4417
|
+
error instanceof Error ? error.message : String(error)
|
|
4418
|
+
);
|
|
4419
|
+
process.exit(1);
|
|
4420
|
+
}
|
|
4421
|
+
});
|
|
4349
4422
|
cli.command("docs [topic]", "Show documentation (topics: setup, rules, config)").action((topic) => {
|
|
4350
4423
|
handleDocs(topic);
|
|
4351
4424
|
});
|