canicode 0.3.0 → 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 CHANGED
@@ -1,6 +1,19 @@
1
- # CanICode
1
+ <p align="center">
2
+ <img src="docs/logo.png" alt="CanICode" width="80">
3
+ </p>
2
4
 
3
- Analyze Figma designs. Score how dev-friendly and AI-friendly they are. Get actionable issues before writing code.
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
- Three ways to use CanICode. Pick one.
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 built-in rule scores, severity, and global settings:
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 ` <div class="flex flex-col items-center group relative">
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
- </div>`;
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 ICON_NAME_PATTERN = /\b(icon|ico|badge|indicator)\b/i;
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 (ICON_NAME_PATTERN.test(node.name)) return false;
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
- See full example: examples/config.json
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
  });