canicode 0.11.5 → 0.12.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +32 -15
- package/dist/cli/index.js +769 -127
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +138 -28
- package/dist/index.js +323 -29
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +353 -43
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +10 -9
- package/package.json +1 -1
- package/skills/canicode-roundtrip/SKILL.md +142 -4
- package/skills/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/canicode-roundtrip/helpers-installer.js +2 -2
- package/skills/canicode-roundtrip/helpers.js +41 -1
- package/skills/cursor/canicode-roundtrip/SKILL.md +142 -4
- package/skills/cursor/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/cursor/canicode-roundtrip/helpers-installer.js +2 -2
- package/skills/cursor/canicode-roundtrip/helpers.js +41 -1
package/dist/index.js
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync } from 'fs';
|
|
3
|
-
import { resolve, join, basename, dirname } from 'path';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync } from 'fs';
|
|
3
|
+
import { resolve, join, basename, dirname, isAbsolute, sep } from 'path';
|
|
4
4
|
import { readFile, writeFile, appendFile } from 'fs/promises';
|
|
5
5
|
import 'crypto';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
|
|
8
8
|
// package.json
|
|
9
|
-
var version = "0.
|
|
9
|
+
var version = "0.12.1";
|
|
10
10
|
var SeveritySchema = z.enum([
|
|
11
11
|
"blocking",
|
|
12
12
|
"risk",
|
|
13
13
|
"missing-info",
|
|
14
|
-
"suggestion"
|
|
14
|
+
"suggestion",
|
|
15
|
+
/**
|
|
16
|
+
* `note` is the zero-impact tier (#519): findings render in the report but
|
|
17
|
+
* never move the grade. Used for annotation-primary rules whose value is the
|
|
18
|
+
* nudge, not the score (e.g. unmapped Code Connect components, info-collection
|
|
19
|
+
* rules whose answers belong in figma-implement-design context, not in linting).
|
|
20
|
+
*/
|
|
21
|
+
"note"
|
|
15
22
|
]);
|
|
16
23
|
var SEVERITY_WEIGHT = {
|
|
17
24
|
blocking: 10,
|
|
18
25
|
risk: 5,
|
|
19
26
|
"missing-info": 2,
|
|
20
|
-
suggestion: 1
|
|
27
|
+
suggestion: 1,
|
|
28
|
+
note: 0
|
|
21
29
|
};
|
|
22
30
|
var SEVERITY_LABELS = {
|
|
23
31
|
blocking: "Blocking",
|
|
24
32
|
risk: "Risk",
|
|
25
33
|
"missing-info": "Missing Info",
|
|
26
|
-
suggestion: "Suggestion"
|
|
34
|
+
suggestion: "Suggestion",
|
|
35
|
+
note: "Note"
|
|
27
36
|
};
|
|
28
37
|
var DetectionSchema = z.literal("rule-based");
|
|
29
38
|
var OutputChannelSchema = z.enum(["score", "annotation"]);
|
|
@@ -298,7 +307,8 @@ var CategoryScoreResultSchema = z.object({
|
|
|
298
307
|
blocking: z.number().int().min(0),
|
|
299
308
|
risk: z.number().int().min(0),
|
|
300
309
|
"missing-info": z.number().int().min(0),
|
|
301
|
-
suggestion: z.number().int().min(0)
|
|
310
|
+
suggestion: z.number().int().min(0),
|
|
311
|
+
note: z.number().int().min(0)
|
|
302
312
|
})
|
|
303
313
|
});
|
|
304
314
|
var McpIssueSchema = z.object({
|
|
@@ -349,7 +359,14 @@ var McpAnalyzeResponseSchema = z.object({
|
|
|
349
359
|
issuesByRule: z.record(z.string(), z.number().int().min(0)),
|
|
350
360
|
issues: z.array(McpIssueSchema),
|
|
351
361
|
summary: z.string(),
|
|
352
|
-
failedRules: z.array(z.string()).optional()
|
|
362
|
+
failedRules: z.array(z.string()).optional(),
|
|
363
|
+
/**
|
|
364
|
+
* #526 sub-task 3 — Code Connect mapping coverage. Optional: only emitted
|
|
365
|
+
* when the consuming repo has `figma.config.json`. Numerator = components in
|
|
366
|
+
* this file with a discovered `figma.connect` declaration; denominator =
|
|
367
|
+
* total components in the file.
|
|
368
|
+
*/
|
|
369
|
+
codeConnectCoverage: z.object({ mapped: z.number().int().min(0), total: z.number().int().min(0) }).optional()
|
|
353
370
|
});
|
|
354
371
|
var GradeSchema2 = z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]);
|
|
355
372
|
var InstanceContextSchema = z.object({
|
|
@@ -486,6 +503,7 @@ var RULE_ID_CATEGORY = {
|
|
|
486
503
|
"detached-instance": "code-quality",
|
|
487
504
|
"variant-structure-mismatch": "code-quality",
|
|
488
505
|
"deep-nesting": "code-quality",
|
|
506
|
+
"unmapped-component": "code-quality",
|
|
489
507
|
// Token Management
|
|
490
508
|
"raw-value": "token-management",
|
|
491
509
|
"irregular-spacing": "token-management",
|
|
@@ -516,6 +534,12 @@ var RULE_PURPOSE = {
|
|
|
516
534
|
"detached-instance": "violation",
|
|
517
535
|
"variant-structure-mismatch": "violation",
|
|
518
536
|
"deep-nesting": "violation",
|
|
537
|
+
// #520: unmapped-component is annotation-primary. Fires only when the
|
|
538
|
+
// user has Code Connect set up at all (figma.config.json present in cwd).
|
|
539
|
+
// The gotcha drives the user to /canicode-roundtrip for actual mapping
|
|
540
|
+
// registration via the Figma MCP tools — analyze itself does not parse
|
|
541
|
+
// mapping declarations (deferred to v1.5).
|
|
542
|
+
"unmapped-component": "info-collection",
|
|
519
543
|
// Token Management
|
|
520
544
|
"raw-value": "violation",
|
|
521
545
|
"irregular-spacing": "violation",
|
|
@@ -559,12 +583,12 @@ var RULE_CONFIGS = {
|
|
|
559
583
|
enabled: true
|
|
560
584
|
},
|
|
561
585
|
"missing-size-constraint": {
|
|
562
|
-
// #403:
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
severity: "
|
|
567
|
-
score:
|
|
586
|
+
// #403 → #519: info-collection rule. Score is 0 (severity `note`):
|
|
587
|
+
// its value is the gotcha annotation, not the grade impact. Survey-
|
|
588
|
+
// generator includes this rule via the `purpose === "info-collection"`
|
|
589
|
+
// branch so the gotcha keeps surfacing.
|
|
590
|
+
severity: "note",
|
|
591
|
+
score: 0,
|
|
568
592
|
enabled: true
|
|
569
593
|
},
|
|
570
594
|
// ── Code Quality ──
|
|
@@ -596,6 +620,16 @@ var RULE_CONFIGS = {
|
|
|
596
620
|
maxDepth: 5
|
|
597
621
|
}
|
|
598
622
|
},
|
|
623
|
+
"unmapped-component": {
|
|
624
|
+
// #520 / #519: zero-impact tier. Fires per main component when Code
|
|
625
|
+
// Connect is set up in the consuming repo (figma.config.json at cwd).
|
|
626
|
+
// Score is 0 because the rule's value is the gotcha + roundtrip handoff,
|
|
627
|
+
// not the grade signal — designers who deliberately do not map (e.g.
|
|
628
|
+
// marketing-only banners) are not punished.
|
|
629
|
+
severity: "note",
|
|
630
|
+
score: 0,
|
|
631
|
+
enabled: true
|
|
632
|
+
},
|
|
599
633
|
// ── Token Management ──
|
|
600
634
|
"raw-value": {
|
|
601
635
|
severity: "missing-info",
|
|
@@ -617,15 +651,15 @@ var RULE_CONFIGS = {
|
|
|
617
651
|
// is minimal. Score stays at -1 so re-enabling `missing-prototype` on
|
|
618
652
|
// fixtures that lack `interactionDestinations` (#139) cannot swing grades.
|
|
619
653
|
"missing-interaction-state": {
|
|
620
|
-
severity: "
|
|
621
|
-
|
|
622
|
-
|
|
654
|
+
severity: "note",
|
|
655
|
+
// #519: info-collection rule, zero-score tier
|
|
656
|
+
score: 0,
|
|
623
657
|
enabled: true
|
|
624
658
|
},
|
|
625
659
|
"missing-prototype": {
|
|
626
|
-
severity: "
|
|
627
|
-
|
|
628
|
-
|
|
660
|
+
severity: "note",
|
|
661
|
+
// #519: info-collection — annotation is primary output, no grade impact
|
|
662
|
+
score: 0,
|
|
629
663
|
enabled: true
|
|
630
664
|
},
|
|
631
665
|
// ── Semantic ──
|
|
@@ -809,11 +843,31 @@ function defineRule(rule) {
|
|
|
809
843
|
ruleRegistry.register(rule);
|
|
810
844
|
return rule;
|
|
811
845
|
}
|
|
812
|
-
var
|
|
846
|
+
var PropertyAcknowledgmentIntentSchema = z.object({
|
|
847
|
+
kind: z.literal("property").default("property"),
|
|
813
848
|
field: z.string(),
|
|
814
849
|
value: z.unknown(),
|
|
815
850
|
scope: z.enum(["instance", "definition"])
|
|
816
851
|
});
|
|
852
|
+
var RuleOptOutAcknowledgmentIntentSchema = z.object({
|
|
853
|
+
kind: z.literal("rule-opt-out"),
|
|
854
|
+
ruleId: z.string()
|
|
855
|
+
}).strict();
|
|
856
|
+
var AcknowledgmentIntentSchema = z.preprocess((raw) => {
|
|
857
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
858
|
+
const obj = raw;
|
|
859
|
+
if (obj["kind"] === void 0) {
|
|
860
|
+
return { ...obj, kind: "property" };
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return raw;
|
|
864
|
+
}, z.discriminatedUnion("kind", [
|
|
865
|
+
PropertyAcknowledgmentIntentSchema,
|
|
866
|
+
RuleOptOutAcknowledgmentIntentSchema
|
|
867
|
+
]));
|
|
868
|
+
function isRuleOptOutIntent(intent) {
|
|
869
|
+
return intent !== void 0 && intent.kind === "rule-opt-out";
|
|
870
|
+
}
|
|
817
871
|
var AcknowledgmentSceneWriteOutcomeSchema = z.object({
|
|
818
872
|
result: z.enum([
|
|
819
873
|
"succeeded",
|
|
@@ -886,6 +940,7 @@ var RuleEngine = class {
|
|
|
886
940
|
excludeNamePattern;
|
|
887
941
|
excludeNodeTypes;
|
|
888
942
|
acknowledgments;
|
|
943
|
+
acknowledgmentsByKey;
|
|
889
944
|
scopeOverride;
|
|
890
945
|
constructor(options = {}) {
|
|
891
946
|
this.configs = options.configs ?? RULE_CONFIGS;
|
|
@@ -894,10 +949,15 @@ var RuleEngine = class {
|
|
|
894
949
|
this.targetNodeId = options.targetNodeId;
|
|
895
950
|
this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
|
|
896
951
|
this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
|
|
952
|
+
const ackList = options.acknowledgments ?? [];
|
|
897
953
|
this.acknowledgments = new Set(
|
|
898
|
-
(
|
|
899
|
-
|
|
900
|
-
|
|
954
|
+
ackList.map((a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`)
|
|
955
|
+
);
|
|
956
|
+
this.acknowledgmentsByKey = new Map(
|
|
957
|
+
ackList.map((a) => [
|
|
958
|
+
`${normalizeNodeId(a.nodeId)}::${a.ruleId}`,
|
|
959
|
+
a
|
|
960
|
+
])
|
|
901
961
|
);
|
|
902
962
|
this.scopeOverride = options.scope;
|
|
903
963
|
}
|
|
@@ -981,6 +1041,7 @@ var RuleEngine = class {
|
|
|
981
1041
|
if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
|
|
982
1042
|
return;
|
|
983
1043
|
}
|
|
1044
|
+
const acknowledgmentsByKey = this.acknowledgmentsByKey;
|
|
984
1045
|
const context = {
|
|
985
1046
|
file,
|
|
986
1047
|
parent,
|
|
@@ -992,7 +1053,8 @@ var RuleEngine = class {
|
|
|
992
1053
|
siblings,
|
|
993
1054
|
analysisState,
|
|
994
1055
|
scope,
|
|
995
|
-
rootNodeType
|
|
1056
|
+
rootNodeType,
|
|
1057
|
+
findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
|
|
996
1058
|
};
|
|
997
1059
|
for (const rule of rules) {
|
|
998
1060
|
const ruleId = rule.definition.id;
|
|
@@ -1086,6 +1148,7 @@ var STRATEGY_BY_RULE = {
|
|
|
1086
1148
|
// Strategy C — annotation only
|
|
1087
1149
|
"absolute-position-in-auto-layout": "annotation",
|
|
1088
1150
|
"variant-structure-mismatch": "annotation",
|
|
1151
|
+
"unmapped-component": "annotation",
|
|
1089
1152
|
// Strategy D — auto-fix lower-severity issues from analyze output
|
|
1090
1153
|
"non-standard-naming": "auto-fix",
|
|
1091
1154
|
"inconsistent-naming-convention": "auto-fix",
|
|
@@ -1120,6 +1183,7 @@ function resolveTargetProperty(ruleId, subType) {
|
|
|
1120
1183
|
case "raw-value":
|
|
1121
1184
|
case "missing-interaction-state":
|
|
1122
1185
|
case "missing-prototype":
|
|
1186
|
+
case "unmapped-component":
|
|
1123
1187
|
return void 0;
|
|
1124
1188
|
}
|
|
1125
1189
|
}
|
|
@@ -1251,6 +1315,7 @@ function calculateScores(result, configs) {
|
|
|
1251
1315
|
risk: 0,
|
|
1252
1316
|
missingInfo: 0,
|
|
1253
1317
|
suggestion: 0,
|
|
1318
|
+
note: 0,
|
|
1254
1319
|
nodeCount,
|
|
1255
1320
|
acknowledgedCount: 0
|
|
1256
1321
|
};
|
|
@@ -1268,6 +1333,9 @@ function calculateScores(result, configs) {
|
|
|
1268
1333
|
case "suggestion":
|
|
1269
1334
|
summary.suggestion++;
|
|
1270
1335
|
break;
|
|
1336
|
+
case "note":
|
|
1337
|
+
summary.note++;
|
|
1338
|
+
break;
|
|
1271
1339
|
}
|
|
1272
1340
|
if (issue.acknowledged === true) summary.acknowledgedCount++;
|
|
1273
1341
|
}
|
|
@@ -1299,7 +1367,8 @@ function initializeCategoryScores() {
|
|
|
1299
1367
|
blocking: 0,
|
|
1300
1368
|
risk: 0,
|
|
1301
1369
|
"missing-info": 0,
|
|
1302
|
-
suggestion: 0
|
|
1370
|
+
suggestion: 0,
|
|
1371
|
+
note: 0
|
|
1303
1372
|
}
|
|
1304
1373
|
};
|
|
1305
1374
|
}
|
|
@@ -1320,6 +1389,7 @@ function formatScoreSummary(report) {
|
|
|
1320
1389
|
lines.push(` Risk: ${report.summary.risk}`);
|
|
1321
1390
|
lines.push(` Missing Info: ${report.summary.missingInfo}`);
|
|
1322
1391
|
lines.push(` Suggestion: ${report.summary.suggestion}`);
|
|
1392
|
+
lines.push(` Note: ${report.summary.note}`);
|
|
1323
1393
|
if (report.summary.acknowledgedCount > 0) {
|
|
1324
1394
|
const unaddressed = report.summary.totalIssues - report.summary.acknowledgedCount;
|
|
1325
1395
|
lines.push(
|
|
@@ -1338,10 +1408,25 @@ function getSeverityLabel(severity) {
|
|
|
1338
1408
|
blocking: "Blocking",
|
|
1339
1409
|
risk: "Risk",
|
|
1340
1410
|
"missing-info": "Missing Info",
|
|
1341
|
-
suggestion: "Suggestion"
|
|
1411
|
+
suggestion: "Suggestion",
|
|
1412
|
+
note: "Note"
|
|
1342
1413
|
};
|
|
1343
1414
|
return labels[severity];
|
|
1344
1415
|
}
|
|
1416
|
+
function formatCodeConnectCoverageLine(coverage) {
|
|
1417
|
+
const { mapped, total } = coverage;
|
|
1418
|
+
const pct = total === 0 ? 0 : Math.round(mapped / total * 100);
|
|
1419
|
+
return `Code Connect coverage: ${mapped}/${total} components (${pct}%) mapped`;
|
|
1420
|
+
}
|
|
1421
|
+
var ROUNDTRIP_OPT_OUT_HINT = "Some components may carry roundtrip-recorded opt-outs that this standalone analyze cannot see (Figma REST annotations field is in private beta). Run /canicode-roundtrip to apply opt-outs.";
|
|
1422
|
+
function formatRoundtripOptOutHintLine(issues, acknowledgmentsProvided) {
|
|
1423
|
+
if (acknowledgmentsProvided) return null;
|
|
1424
|
+
const hasUnmapped = issues.some(
|
|
1425
|
+
(issue) => issue.violation.ruleId === "unmapped-component"
|
|
1426
|
+
);
|
|
1427
|
+
if (!hasUnmapped) return null;
|
|
1428
|
+
return ROUNDTRIP_OPT_OUT_HINT;
|
|
1429
|
+
}
|
|
1345
1430
|
function buildResultJson(fileName, result, scores, options) {
|
|
1346
1431
|
const issuesByRule = {};
|
|
1347
1432
|
for (const issue of result.issues) {
|
|
@@ -1371,6 +1456,14 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
1371
1456
|
...issue.acknowledged === true ? { acknowledged: true } : {}
|
|
1372
1457
|
};
|
|
1373
1458
|
});
|
|
1459
|
+
const optOutHint = options?.roundtripOptOutHintEligible ? formatRoundtripOptOutHintLine(result.issues, false) : null;
|
|
1460
|
+
const summaryParts = [formatScoreSummary(scores)];
|
|
1461
|
+
if (options?.codeConnectCoverage) {
|
|
1462
|
+
summaryParts.push(formatCodeConnectCoverageLine(options.codeConnectCoverage));
|
|
1463
|
+
}
|
|
1464
|
+
if (optOutHint) {
|
|
1465
|
+
summaryParts.push(optOutHint);
|
|
1466
|
+
}
|
|
1374
1467
|
const json = {
|
|
1375
1468
|
version,
|
|
1376
1469
|
analyzedAt: result.analyzedAt,
|
|
@@ -1390,8 +1483,14 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
1390
1483
|
},
|
|
1391
1484
|
issuesByRule,
|
|
1392
1485
|
issues,
|
|
1393
|
-
summary:
|
|
1486
|
+
summary: summaryParts.join("\n\n")
|
|
1394
1487
|
};
|
|
1488
|
+
if (options?.codeConnectCoverage) {
|
|
1489
|
+
json["codeConnectCoverage"] = options.codeConnectCoverage;
|
|
1490
|
+
}
|
|
1491
|
+
if (optOutHint) {
|
|
1492
|
+
json["roundtripOptOutHint"] = optOutHint;
|
|
1493
|
+
}
|
|
1395
1494
|
if (result.failedRules.length > 0) {
|
|
1396
1495
|
json["failedRules"] = result.failedRules;
|
|
1397
1496
|
}
|
|
@@ -2455,6 +2554,10 @@ var missingComponentMsg = {
|
|
|
2455
2554
|
suggestion: `Create a new variant for this style combination`
|
|
2456
2555
|
})
|
|
2457
2556
|
};
|
|
2557
|
+
var unmappedComponentMsg = (componentName) => ({
|
|
2558
|
+
message: `"${componentName}" has no Code Connect mapping`,
|
|
2559
|
+
suggestion: `Run /canicode-roundtrip on this component to register a mapping so figma-implement-design reuses your code instead of regenerating markup. Skip if intentionally unmapped.`
|
|
2560
|
+
});
|
|
2458
2561
|
var detachedInstanceMsg = (name, componentName) => ({
|
|
2459
2562
|
message: `"${name}" may be a detached instance of component "${componentName}"`,
|
|
2460
2563
|
suggestion: `Restore as an instance of "${componentName}" or create a new variant`
|
|
@@ -3006,6 +3109,128 @@ var irregularSpacing = defineRule({
|
|
|
3006
3109
|
definition: irregularSpacingDef,
|
|
3007
3110
|
check: irregularSpacingCheck
|
|
3008
3111
|
});
|
|
3112
|
+
var FIGMA_CONFIG_FILENAME = "figma.config.json";
|
|
3113
|
+
var FIGMA_CONNECT_FILE_GLOB = /\.figma\.(tsx?|jsx?)$/;
|
|
3114
|
+
var NODE_ID_QUERY_RE = /[?&]node-id=([0-9A-Za-z%:\-_]+)/;
|
|
3115
|
+
function parseCodeConnectMappings(cwd) {
|
|
3116
|
+
const configPath = join(cwd, FIGMA_CONFIG_FILENAME);
|
|
3117
|
+
if (!existsSync(configPath)) {
|
|
3118
|
+
return {
|
|
3119
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3120
|
+
scannedFiles: [],
|
|
3121
|
+
skipReason: "no-config",
|
|
3122
|
+
skippedReason: `${FIGMA_CONFIG_FILENAME} not found at ${cwd}`
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
let config;
|
|
3126
|
+
try {
|
|
3127
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
3128
|
+
} catch (err) {
|
|
3129
|
+
return {
|
|
3130
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3131
|
+
scannedFiles: [],
|
|
3132
|
+
skipReason: "malformed-config",
|
|
3133
|
+
skippedReason: `malformed ${FIGMA_CONFIG_FILENAME}: ${err.message}`
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
const includes = config.codeConnect?.include ?? config.include ?? [];
|
|
3137
|
+
if (includes.length === 0) {
|
|
3138
|
+
return {
|
|
3139
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3140
|
+
scannedFiles: [],
|
|
3141
|
+
skipReason: "no-includes",
|
|
3142
|
+
skippedReason: `${FIGMA_CONFIG_FILENAME} has no codeConnect.include paths`
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
const candidateFiles = /* @__PURE__ */ new Set();
|
|
3146
|
+
for (const includePattern of includes) {
|
|
3147
|
+
for (const file of resolveInclude(cwd, includePattern)) {
|
|
3148
|
+
candidateFiles.add(file);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
const mappedNodeIds = /* @__PURE__ */ new Set();
|
|
3152
|
+
const scannedFiles = [];
|
|
3153
|
+
for (const file of candidateFiles) {
|
|
3154
|
+
scannedFiles.push(file);
|
|
3155
|
+
let contents;
|
|
3156
|
+
try {
|
|
3157
|
+
contents = readFileSync(file, "utf-8");
|
|
3158
|
+
} catch {
|
|
3159
|
+
continue;
|
|
3160
|
+
}
|
|
3161
|
+
for (const nodeId of extractNodeIdsFromSource(contents)) {
|
|
3162
|
+
mappedNodeIds.add(nodeId);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
return { mappedNodeIds, scannedFiles };
|
|
3166
|
+
}
|
|
3167
|
+
function resolveInclude(cwd, includePattern) {
|
|
3168
|
+
const results = [];
|
|
3169
|
+
const absolute = isAbsolute(includePattern) ? includePattern : resolve(cwd, includePattern);
|
|
3170
|
+
const segments = absolute.split(sep);
|
|
3171
|
+
let firstGlobIdx = segments.findIndex((s) => /[*?{[]/.test(s));
|
|
3172
|
+
if (firstGlobIdx === -1) {
|
|
3173
|
+
if (existsSync(absolute)) {
|
|
3174
|
+
const stat = statSync(absolute);
|
|
3175
|
+
if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(absolute)) {
|
|
3176
|
+
results.push(absolute);
|
|
3177
|
+
} else if (stat.isDirectory()) {
|
|
3178
|
+
walkDir(absolute, results);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
return results;
|
|
3182
|
+
}
|
|
3183
|
+
const rootSegments = segments.slice(0, firstGlobIdx);
|
|
3184
|
+
const root = rootSegments.length === 0 ? sep : rootSegments.join(sep);
|
|
3185
|
+
if (!existsSync(root)) return results;
|
|
3186
|
+
const rootStat = statSync(root);
|
|
3187
|
+
if (!rootStat.isDirectory()) return results;
|
|
3188
|
+
walkDir(root, results);
|
|
3189
|
+
const prefix = rootSegments.join(sep) + sep;
|
|
3190
|
+
return results.filter((f) => f.startsWith(prefix) || rootSegments.length === 0);
|
|
3191
|
+
}
|
|
3192
|
+
function walkDir(dir, out) {
|
|
3193
|
+
let entries;
|
|
3194
|
+
try {
|
|
3195
|
+
entries = readdirSync(dir);
|
|
3196
|
+
} catch {
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
for (const entry of entries) {
|
|
3200
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
3201
|
+
const full = join(dir, entry);
|
|
3202
|
+
let stat;
|
|
3203
|
+
try {
|
|
3204
|
+
stat = statSync(full);
|
|
3205
|
+
} catch {
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
if (stat.isDirectory()) {
|
|
3209
|
+
walkDir(full, out);
|
|
3210
|
+
} else if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(full)) {
|
|
3211
|
+
out.push(full);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
function extractNodeIdsFromSource(source) {
|
|
3216
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
3217
|
+
const re = new RegExp(NODE_ID_QUERY_RE, "g");
|
|
3218
|
+
let match;
|
|
3219
|
+
while ((match = re.exec(source)) !== null) {
|
|
3220
|
+
const raw = match[1];
|
|
3221
|
+
if (!raw) continue;
|
|
3222
|
+
const decoded = safeDecode(raw);
|
|
3223
|
+
nodeIds.add(decoded.replace(/-/g, ":"));
|
|
3224
|
+
}
|
|
3225
|
+
return nodeIds;
|
|
3226
|
+
}
|
|
3227
|
+
function safeDecode(raw) {
|
|
3228
|
+
try {
|
|
3229
|
+
return decodeURIComponent(raw);
|
|
3230
|
+
} catch {
|
|
3231
|
+
return raw;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3009
3234
|
|
|
3010
3235
|
// src/core/rules/component/index.ts
|
|
3011
3236
|
var STYLE_COMPARE_KEYS = ["fills", "strokes", "effects", "cornerRadius", "strokeWeight", "individualStrokeWeights"];
|
|
@@ -3215,6 +3440,49 @@ var variantStructureMismatch = defineRule({
|
|
|
3215
3440
|
definition: variantStructureMismatchDef,
|
|
3216
3441
|
check: variantStructureMismatchCheck
|
|
3217
3442
|
});
|
|
3443
|
+
var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
|
|
3444
|
+
var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
|
|
3445
|
+
function codeConnectIsSetUp(context) {
|
|
3446
|
+
return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
|
|
3447
|
+
return existsSync(join(process.cwd(), "figma.config.json"));
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
function codeConnectMappings(context) {
|
|
3451
|
+
return getAnalysisState(
|
|
3452
|
+
context,
|
|
3453
|
+
CODE_CONNECT_MAPPINGS_KEY,
|
|
3454
|
+
() => parseCodeConnectMappings(process.cwd())
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
var unmappedComponentDef = {
|
|
3458
|
+
id: "unmapped-component",
|
|
3459
|
+
name: "Unmapped Component",
|
|
3460
|
+
category: "code-quality",
|
|
3461
|
+
why: "Without a Code Connect mapping, figma-implement-design regenerates the same markup every time this component appears in a screen \u2014 wasting tokens and risking drift.",
|
|
3462
|
+
impact: "Future roundtrips on screens containing this component cannot reuse your existing code; they regenerate markup that may not match the canonical implementation.",
|
|
3463
|
+
fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
|
|
3464
|
+
};
|
|
3465
|
+
var unmappedComponentCheck = (node, context) => {
|
|
3466
|
+
if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
|
|
3467
|
+
if (isInsideInstance(context)) return null;
|
|
3468
|
+
if (!codeConnectIsSetUp(context)) return null;
|
|
3469
|
+
const mappings = codeConnectMappings(context);
|
|
3470
|
+
if (mappings.mappedNodeIds.has(node.id)) return null;
|
|
3471
|
+
const ack = context.findAcknowledgment(node.id, unmappedComponentDef.id);
|
|
3472
|
+
if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
|
|
3473
|
+
return null;
|
|
3474
|
+
}
|
|
3475
|
+
return {
|
|
3476
|
+
ruleId: unmappedComponentDef.id,
|
|
3477
|
+
nodeId: node.id,
|
|
3478
|
+
nodePath: context.path.join(" > "),
|
|
3479
|
+
...unmappedComponentMsg(node.name)
|
|
3480
|
+
};
|
|
3481
|
+
};
|
|
3482
|
+
var unmappedComponent = defineRule({
|
|
3483
|
+
definition: unmappedComponentDef,
|
|
3484
|
+
check: unmappedComponentCheck
|
|
3485
|
+
});
|
|
3218
3486
|
|
|
3219
3487
|
// src/core/rules/naming/index.ts
|
|
3220
3488
|
function capitalize(s) {
|
|
@@ -3718,6 +3986,32 @@ var FigmaClient = class _FigmaClient {
|
|
|
3718
3986
|
const buffer = await response.arrayBuffer();
|
|
3719
3987
|
return Buffer.from(buffer).toString("base64");
|
|
3720
3988
|
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Get the components a file has published to a team library.
|
|
3991
|
+
*
|
|
3992
|
+
* `GET /v1/files/:file_key/components` returns only components that have
|
|
3993
|
+
* been pushed via the Publish Library action — local-but-unpublished
|
|
3994
|
+
* components are absent. This is the authoritative way to detect whether
|
|
3995
|
+
* a Figma component is mappable via Code Connect (#532): `add_code_connect_map`
|
|
3996
|
+
* requires a published component and otherwise fails with "Published
|
|
3997
|
+
* component not found."
|
|
3998
|
+
*/
|
|
3999
|
+
async getPublishedComponents(fileKey) {
|
|
4000
|
+
const url = `${FIGMA_API_BASE}/files/${fileKey}/components`;
|
|
4001
|
+
const response = await fetch(url, {
|
|
4002
|
+
headers: { "X-Figma-Token": this.token }
|
|
4003
|
+
});
|
|
4004
|
+
if (!response.ok) {
|
|
4005
|
+
const error = await response.json().catch(() => ({}));
|
|
4006
|
+
throw new FigmaClientError(
|
|
4007
|
+
`Failed to fetch published components: ${response.status} ${response.statusText}`,
|
|
4008
|
+
response.status,
|
|
4009
|
+
error
|
|
4010
|
+
);
|
|
4011
|
+
}
|
|
4012
|
+
const data = await response.json();
|
|
4013
|
+
return data.meta?.components ?? [];
|
|
4014
|
+
}
|
|
3721
4015
|
async getFileNodes(fileKey, nodeIds) {
|
|
3722
4016
|
const ids = nodeIds.join(",");
|
|
3723
4017
|
const url = `${FIGMA_API_BASE}/files/${fileKey}/nodes?ids=${encodeURIComponent(ids)}`;
|
|
@@ -5360,6 +5654,6 @@ var ActivityLogger = class {
|
|
|
5360
5654
|
}
|
|
5361
5655
|
};
|
|
5362
5656
|
|
|
5363
|
-
export { ALL_STRIP_TYPES, ActivityLogger, AnalysisFileSchema, AnalysisNodeSchema, AnalysisNodeTypeSchema, AnalysisScopeSchema, AnnotationPropertySchema, CATEGORIES, CATEGORY_LABELS, CalibrationConfigSchema, CalibrationStatusSchema, CategorySchema, CategoryScoreSchema, ConfidenceSchema, ConversionRecordSchema, DEFAULT_CODEGEN_READY_MIN_GRADE, DEPTH_WEIGHT_CATEGORIES, DESIGN_TREE_INFO_TYPES, DetectionSchema, DifficultySchema, FigmaClient, FigmaClientError, FigmaFileLoadError, FigmaUrlInfoSchema, FigmaUrlParseError, GRADE_ORDER, GapAnalyzerOutputSchema, GapEntrySchema, GotchaDetectionSchema, GotchaOutputChannelSchema, GotchaPersistenceIntentSchema, GotchaSurveyQuestionSchema, GotchaSurveySchema, GridChildAlignSchema, GroupedSurveySchema, InstanceContextSchema, IssueSchema, LayoutAlignSchema, LayoutConstraintSchema, LayoutModeSchema, LayoutPositioningSchema, LayoutWrapSchema, McpAnalyzeResponseSchema, MismatchCaseSchema, MismatchTypeSchema, NewRuleProposalSchema, NodeIssueSummarySchema, OutputChannelSchema, OverflowDirectionSchema, PersistenceIntentSchema, RULE_ANNOTATION_PROPERTIES, RULE_CONFIGS, RULE_ID_CATEGORY, RULE_PURPOSE, ReportMetadataSchema, ReportSchema, RuleApplyStrategySchema, RuleConfigSchema, RuleDefinitionSchema, RuleEngine, RuleImpactAssessmentSchema, RulePurposeSchema, RuleRelatedStruggleSchema, SEVERITY_LABELS, SEVERITY_WEIGHT, SamplingStrategySchema, ScoreAdjustmentSchema, SeveritySchema, StripDeltaResultSchema, StripDeltasArraySchema, StripTypeEnum, SurveyQuestionBatchSchema, SurveyQuestionGroupSchema, UncoveredStruggleSchema, UncoveredStrugglesInputSchema, version as VERSION, VisualCompareCliOptionsSchema, absolutePositionInAutoLayout, analyzeFile, buildFigmaDeepLink, buildResultJson, calculateScores, collectComponentIds, collectInteractionDestinationIds, createRuleEngine, deepNesting, defineRule, detachedInstance, detectAnalysisScope, extractRuleScores, fixedSizeInAutoLayout, formatScoreSummary, generateCalibrationReport, generateDesignTree, generateDesignTreeWithStats, getAnalysisState, getAnnotationProperties, getCategoryLabel, getConfigsWithPreset, getRuleOption, getRulePurpose, getSeverityLabel, gradeToClassName, inconsistentNamingConvention, irregularSpacing, isInstanceChildNodeId, isReadyForCodeGen, loadFigmaFileFromJson, missingComponent, missingInteractionState, missingPrototype, missingSizeConstraint, noAutoLayout, nonLayoutContainer, nonSemanticName, nonStandardNaming, parseFigmaJson, parseFigmaUrl, parseInstanceChildNodeId, rawValue, resolveComponentDefinitions, resolveGotchaApplyTarget, resolveInteractionDestinations, ruleRegistry, runAnalysisAgent, runCalibrationAnalyze, runCalibrationEvaluate, runEvaluationAgent, runTuningAgent, stripDeltaToDifficulty, stripDesignTree, supportsDepthWeight, toCommentableNodeId, tokenDeltaToDifficulty, transformComponentMasterNodes, transformFigmaResponse, transformFileNodesResponse, variantStructureMismatch };
|
|
5657
|
+
export { ALL_STRIP_TYPES, ActivityLogger, AnalysisFileSchema, AnalysisNodeSchema, AnalysisNodeTypeSchema, AnalysisScopeSchema, AnnotationPropertySchema, CATEGORIES, CATEGORY_LABELS, CalibrationConfigSchema, CalibrationStatusSchema, CategorySchema, CategoryScoreSchema, ConfidenceSchema, ConversionRecordSchema, DEFAULT_CODEGEN_READY_MIN_GRADE, DEPTH_WEIGHT_CATEGORIES, DESIGN_TREE_INFO_TYPES, DetectionSchema, DifficultySchema, FigmaClient, FigmaClientError, FigmaFileLoadError, FigmaUrlInfoSchema, FigmaUrlParseError, GRADE_ORDER, GapAnalyzerOutputSchema, GapEntrySchema, GotchaDetectionSchema, GotchaOutputChannelSchema, GotchaPersistenceIntentSchema, GotchaSurveyQuestionSchema, GotchaSurveySchema, GridChildAlignSchema, GroupedSurveySchema, InstanceContextSchema, IssueSchema, LayoutAlignSchema, LayoutConstraintSchema, LayoutModeSchema, LayoutPositioningSchema, LayoutWrapSchema, McpAnalyzeResponseSchema, MismatchCaseSchema, MismatchTypeSchema, NewRuleProposalSchema, NodeIssueSummarySchema, OutputChannelSchema, OverflowDirectionSchema, PersistenceIntentSchema, ROUNDTRIP_OPT_OUT_HINT, RULE_ANNOTATION_PROPERTIES, RULE_CONFIGS, RULE_ID_CATEGORY, RULE_PURPOSE, ReportMetadataSchema, ReportSchema, RuleApplyStrategySchema, RuleConfigSchema, RuleDefinitionSchema, RuleEngine, RuleImpactAssessmentSchema, RulePurposeSchema, RuleRelatedStruggleSchema, SEVERITY_LABELS, SEVERITY_WEIGHT, SamplingStrategySchema, ScoreAdjustmentSchema, SeveritySchema, StripDeltaResultSchema, StripDeltasArraySchema, StripTypeEnum, SurveyQuestionBatchSchema, SurveyQuestionGroupSchema, UncoveredStruggleSchema, UncoveredStrugglesInputSchema, version as VERSION, VisualCompareCliOptionsSchema, absolutePositionInAutoLayout, analyzeFile, buildFigmaDeepLink, buildResultJson, calculateScores, collectComponentIds, collectInteractionDestinationIds, createRuleEngine, deepNesting, defineRule, detachedInstance, detectAnalysisScope, extractRuleScores, fixedSizeInAutoLayout, formatCodeConnectCoverageLine, formatRoundtripOptOutHintLine, formatScoreSummary, generateCalibrationReport, generateDesignTree, generateDesignTreeWithStats, getAnalysisState, getAnnotationProperties, getCategoryLabel, getConfigsWithPreset, getRuleOption, getRulePurpose, getSeverityLabel, gradeToClassName, inconsistentNamingConvention, irregularSpacing, isInstanceChildNodeId, isReadyForCodeGen, loadFigmaFileFromJson, missingComponent, missingInteractionState, missingPrototype, missingSizeConstraint, noAutoLayout, nonLayoutContainer, nonSemanticName, nonStandardNaming, parseFigmaJson, parseFigmaUrl, parseInstanceChildNodeId, rawValue, resolveComponentDefinitions, resolveGotchaApplyTarget, resolveInteractionDestinations, ruleRegistry, runAnalysisAgent, runCalibrationAnalyze, runCalibrationEvaluate, runEvaluationAgent, runTuningAgent, stripDeltaToDifficulty, stripDesignTree, supportsDepthWeight, toCommentableNodeId, tokenDeltaToDifficulty, transformComponentMasterNodes, transformFigmaResponse, transformFileNodesResponse, unmappedComponent, variantStructureMismatch };
|
|
5364
5658
|
//# sourceMappingURL=index.js.map
|
|
5365
5659
|
//# sourceMappingURL=index.js.map
|