canicode 0.12.0 → 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/dist/cli/index.js +394 -47
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +119 -27
- package/dist/index.js +238 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +252 -15
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/canicode-roundtrip/SKILL.md +46 -8
- 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 +46 -8
- 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/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, readdirSync, renameSync, chmodSync, copyFileSync } from 'fs';
|
|
3
|
-
import { join, resolve, dirname, basename, relative } from 'path';
|
|
3
|
+
import { join, resolve, dirname, basename, isAbsolute, sep, relative } from 'path';
|
|
4
4
|
import pixelmatch from 'pixelmatch';
|
|
5
5
|
import { PNG } from 'pngjs';
|
|
6
6
|
import { createRequire } from 'module';
|
|
@@ -132,6 +132,32 @@ var init_figma_client = __esm({
|
|
|
132
132
|
const buffer = await response.arrayBuffer();
|
|
133
133
|
return Buffer.from(buffer).toString("base64");
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Get the components a file has published to a team library.
|
|
137
|
+
*
|
|
138
|
+
* `GET /v1/files/:file_key/components` returns only components that have
|
|
139
|
+
* been pushed via the Publish Library action — local-but-unpublished
|
|
140
|
+
* components are absent. This is the authoritative way to detect whether
|
|
141
|
+
* a Figma component is mappable via Code Connect (#532): `add_code_connect_map`
|
|
142
|
+
* requires a published component and otherwise fails with "Published
|
|
143
|
+
* component not found."
|
|
144
|
+
*/
|
|
145
|
+
async getPublishedComponents(fileKey) {
|
|
146
|
+
const url = `${FIGMA_API_BASE}/files/${fileKey}/components`;
|
|
147
|
+
const response = await fetch(url, {
|
|
148
|
+
headers: { "X-Figma-Token": this.token }
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const error = await response.json().catch(() => ({}));
|
|
152
|
+
throw new FigmaClientError(
|
|
153
|
+
`Failed to fetch published components: ${response.status} ${response.statusText}`,
|
|
154
|
+
response.status,
|
|
155
|
+
error
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const data = await response.json();
|
|
159
|
+
return data.meta?.components ?? [];
|
|
160
|
+
}
|
|
135
161
|
async getFileNodes(fileKey, nodeIds) {
|
|
136
162
|
const ids = nodeIds.join(",");
|
|
137
163
|
const url = `${FIGMA_API_BASE}/files/${fileKey}/nodes?ids=${encodeURIComponent(ids)}`;
|
|
@@ -3053,6 +3079,176 @@ defineRule({
|
|
|
3053
3079
|
definition: irregularSpacingDef,
|
|
3054
3080
|
check: irregularSpacingCheck
|
|
3055
3081
|
});
|
|
3082
|
+
var FIGMA_CONFIG_FILENAME = "figma.config.json";
|
|
3083
|
+
var FIGMA_CONNECT_FILE_GLOB = /\.figma\.(tsx?|jsx?)$/;
|
|
3084
|
+
var NODE_ID_QUERY_RE = /[?&]node-id=([0-9A-Za-z%:\-_]+)/;
|
|
3085
|
+
function parseCodeConnectMappings(cwd) {
|
|
3086
|
+
const configPath = join(cwd, FIGMA_CONFIG_FILENAME);
|
|
3087
|
+
if (!existsSync(configPath)) {
|
|
3088
|
+
return {
|
|
3089
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3090
|
+
scannedFiles: [],
|
|
3091
|
+
skipReason: "no-config",
|
|
3092
|
+
skippedReason: `${FIGMA_CONFIG_FILENAME} not found at ${cwd}`
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
let config2;
|
|
3096
|
+
try {
|
|
3097
|
+
config2 = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
return {
|
|
3100
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3101
|
+
scannedFiles: [],
|
|
3102
|
+
skipReason: "malformed-config",
|
|
3103
|
+
skippedReason: `malformed ${FIGMA_CONFIG_FILENAME}: ${err.message}`
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
const includes = config2.codeConnect?.include ?? config2.include ?? [];
|
|
3107
|
+
if (includes.length === 0) {
|
|
3108
|
+
return {
|
|
3109
|
+
mappedNodeIds: /* @__PURE__ */ new Set(),
|
|
3110
|
+
scannedFiles: [],
|
|
3111
|
+
skipReason: "no-includes",
|
|
3112
|
+
skippedReason: `${FIGMA_CONFIG_FILENAME} has no codeConnect.include paths`
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
const candidateFiles = /* @__PURE__ */ new Set();
|
|
3116
|
+
for (const includePattern of includes) {
|
|
3117
|
+
for (const file of resolveInclude(cwd, includePattern)) {
|
|
3118
|
+
candidateFiles.add(file);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
const mappedNodeIds = /* @__PURE__ */ new Set();
|
|
3122
|
+
const scannedFiles = [];
|
|
3123
|
+
for (const file of candidateFiles) {
|
|
3124
|
+
scannedFiles.push(file);
|
|
3125
|
+
let contents;
|
|
3126
|
+
try {
|
|
3127
|
+
contents = readFileSync(file, "utf-8");
|
|
3128
|
+
} catch {
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
for (const nodeId of extractNodeIdsFromSource(contents)) {
|
|
3132
|
+
mappedNodeIds.add(nodeId);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
return { mappedNodeIds, scannedFiles };
|
|
3136
|
+
}
|
|
3137
|
+
function resolveInclude(cwd, includePattern) {
|
|
3138
|
+
const results = [];
|
|
3139
|
+
const absolute = isAbsolute(includePattern) ? includePattern : resolve(cwd, includePattern);
|
|
3140
|
+
const segments = absolute.split(sep);
|
|
3141
|
+
let firstGlobIdx = segments.findIndex((s) => /[*?{[]/.test(s));
|
|
3142
|
+
if (firstGlobIdx === -1) {
|
|
3143
|
+
if (existsSync(absolute)) {
|
|
3144
|
+
const stat = statSync(absolute);
|
|
3145
|
+
if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(absolute)) {
|
|
3146
|
+
results.push(absolute);
|
|
3147
|
+
} else if (stat.isDirectory()) {
|
|
3148
|
+
walkDir(absolute, results);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
return results;
|
|
3152
|
+
}
|
|
3153
|
+
const rootSegments = segments.slice(0, firstGlobIdx);
|
|
3154
|
+
const root = rootSegments.length === 0 ? sep : rootSegments.join(sep);
|
|
3155
|
+
if (!existsSync(root)) return results;
|
|
3156
|
+
const rootStat = statSync(root);
|
|
3157
|
+
if (!rootStat.isDirectory()) return results;
|
|
3158
|
+
walkDir(root, results);
|
|
3159
|
+
const prefix = rootSegments.join(sep) + sep;
|
|
3160
|
+
return results.filter((f) => f.startsWith(prefix) || rootSegments.length === 0);
|
|
3161
|
+
}
|
|
3162
|
+
function walkDir(dir, out) {
|
|
3163
|
+
let entries;
|
|
3164
|
+
try {
|
|
3165
|
+
entries = readdirSync(dir);
|
|
3166
|
+
} catch {
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
for (const entry of entries) {
|
|
3170
|
+
if (entry === "node_modules" || entry.startsWith(".")) continue;
|
|
3171
|
+
const full = join(dir, entry);
|
|
3172
|
+
let stat;
|
|
3173
|
+
try {
|
|
3174
|
+
stat = statSync(full);
|
|
3175
|
+
} catch {
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
if (stat.isDirectory()) {
|
|
3179
|
+
walkDir(full, out);
|
|
3180
|
+
} else if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(full)) {
|
|
3181
|
+
out.push(full);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
function extractNodeIdsFromSource(source) {
|
|
3186
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
3187
|
+
const re = new RegExp(NODE_ID_QUERY_RE, "g");
|
|
3188
|
+
let match;
|
|
3189
|
+
while ((match = re.exec(source)) !== null) {
|
|
3190
|
+
const raw = match[1];
|
|
3191
|
+
if (!raw) continue;
|
|
3192
|
+
const decoded = safeDecode(raw);
|
|
3193
|
+
nodeIds.add(decoded.replace(/-/g, ":"));
|
|
3194
|
+
}
|
|
3195
|
+
return nodeIds;
|
|
3196
|
+
}
|
|
3197
|
+
function safeDecode(raw) {
|
|
3198
|
+
try {
|
|
3199
|
+
return decodeURIComponent(raw);
|
|
3200
|
+
} catch {
|
|
3201
|
+
return raw;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
var PropertyAcknowledgmentIntentSchema = z.object({
|
|
3205
|
+
kind: z.literal("property").default("property"),
|
|
3206
|
+
field: z.string(),
|
|
3207
|
+
value: z.unknown(),
|
|
3208
|
+
scope: z.enum(["instance", "definition"])
|
|
3209
|
+
});
|
|
3210
|
+
var RuleOptOutAcknowledgmentIntentSchema = z.object({
|
|
3211
|
+
kind: z.literal("rule-opt-out"),
|
|
3212
|
+
ruleId: z.string()
|
|
3213
|
+
}).strict();
|
|
3214
|
+
var AcknowledgmentIntentSchema = z.preprocess((raw) => {
|
|
3215
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
3216
|
+
const obj = raw;
|
|
3217
|
+
if (obj["kind"] === void 0) {
|
|
3218
|
+
return { ...obj, kind: "property" };
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
return raw;
|
|
3222
|
+
}, z.discriminatedUnion("kind", [
|
|
3223
|
+
PropertyAcknowledgmentIntentSchema,
|
|
3224
|
+
RuleOptOutAcknowledgmentIntentSchema
|
|
3225
|
+
]));
|
|
3226
|
+
function isRuleOptOutIntent(intent) {
|
|
3227
|
+
return intent !== void 0 && intent.kind === "rule-opt-out";
|
|
3228
|
+
}
|
|
3229
|
+
var AcknowledgmentSceneWriteOutcomeSchema = z.object({
|
|
3230
|
+
result: z.enum([
|
|
3231
|
+
"succeeded",
|
|
3232
|
+
"silent-ignored",
|
|
3233
|
+
"api-rejected",
|
|
3234
|
+
"user-declined-propagation",
|
|
3235
|
+
"unknown"
|
|
3236
|
+
]),
|
|
3237
|
+
reason: z.string().optional()
|
|
3238
|
+
});
|
|
3239
|
+
var AcknowledgmentSchema = z.object({
|
|
3240
|
+
nodeId: z.string(),
|
|
3241
|
+
ruleId: z.string(),
|
|
3242
|
+
intent: AcknowledgmentIntentSchema.optional(),
|
|
3243
|
+
sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
|
|
3244
|
+
codegenDirective: z.string().optional()
|
|
3245
|
+
});
|
|
3246
|
+
var AcknowledgmentListSchema = z.array(AcknowledgmentSchema);
|
|
3247
|
+
function normalizeNodeId(id) {
|
|
3248
|
+
return id.replace(/-/g, ":");
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// src/core/rules/component/index.ts
|
|
3056
3252
|
var STYLE_COMPARE_KEYS = ["fills", "strokes", "effects", "cornerRadius", "strokeWeight", "individualStrokeWeights"];
|
|
3057
3253
|
function detectStyleOverrides(master, instance) {
|
|
3058
3254
|
const overrides = [];
|
|
@@ -3261,11 +3457,19 @@ defineRule({
|
|
|
3261
3457
|
check: variantStructureMismatchCheck
|
|
3262
3458
|
});
|
|
3263
3459
|
var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
|
|
3460
|
+
var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
|
|
3264
3461
|
function codeConnectIsSetUp(context) {
|
|
3265
3462
|
return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
|
|
3266
3463
|
return existsSync(join(process.cwd(), "figma.config.json"));
|
|
3267
3464
|
});
|
|
3268
3465
|
}
|
|
3466
|
+
function codeConnectMappings(context) {
|
|
3467
|
+
return getAnalysisState(
|
|
3468
|
+
context,
|
|
3469
|
+
CODE_CONNECT_MAPPINGS_KEY,
|
|
3470
|
+
() => parseCodeConnectMappings(process.cwd())
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3269
3473
|
var unmappedComponentDef = {
|
|
3270
3474
|
id: "unmapped-component",
|
|
3271
3475
|
name: "Unmapped Component",
|
|
@@ -3278,6 +3482,12 @@ var unmappedComponentCheck = (node, context) => {
|
|
|
3278
3482
|
if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
|
|
3279
3483
|
if (isInsideInstance(context)) return null;
|
|
3280
3484
|
if (!codeConnectIsSetUp(context)) return null;
|
|
3485
|
+
const mappings = codeConnectMappings(context);
|
|
3486
|
+
if (mappings.mappedNodeIds.has(node.id)) return null;
|
|
3487
|
+
const ack = context.findAcknowledgment(node.id, unmappedComponentDef.id);
|
|
3488
|
+
if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3281
3491
|
return {
|
|
3282
3492
|
ruleId: unmappedComponentDef.id,
|
|
3283
3493
|
nodeId: node.id,
|
|
@@ -3645,32 +3855,6 @@ defineRule({
|
|
|
3645
3855
|
definition: missingPrototypeDef,
|
|
3646
3856
|
check: missingPrototypeCheck
|
|
3647
3857
|
});
|
|
3648
|
-
var AcknowledgmentIntentSchema = z.object({
|
|
3649
|
-
field: z.string(),
|
|
3650
|
-
value: z.unknown(),
|
|
3651
|
-
scope: z.enum(["instance", "definition"])
|
|
3652
|
-
});
|
|
3653
|
-
var AcknowledgmentSceneWriteOutcomeSchema = z.object({
|
|
3654
|
-
result: z.enum([
|
|
3655
|
-
"succeeded",
|
|
3656
|
-
"silent-ignored",
|
|
3657
|
-
"api-rejected",
|
|
3658
|
-
"user-declined-propagation",
|
|
3659
|
-
"unknown"
|
|
3660
|
-
]),
|
|
3661
|
-
reason: z.string().optional()
|
|
3662
|
-
});
|
|
3663
|
-
var AcknowledgmentSchema = z.object({
|
|
3664
|
-
nodeId: z.string(),
|
|
3665
|
-
ruleId: z.string(),
|
|
3666
|
-
intent: AcknowledgmentIntentSchema.optional(),
|
|
3667
|
-
sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
|
|
3668
|
-
codegenDirective: z.string().optional()
|
|
3669
|
-
});
|
|
3670
|
-
var AcknowledgmentListSchema = z.array(AcknowledgmentSchema);
|
|
3671
|
-
function normalizeNodeId(id) {
|
|
3672
|
-
return id.replace(/-/g, ":");
|
|
3673
|
-
}
|
|
3674
3858
|
var AnalysisScopeSchema = z.enum(["page", "component"]);
|
|
3675
3859
|
var COMPONENT_SCOPE_ROOT_TYPES = /* @__PURE__ */ new Set(["COMPONENT", "COMPONENT_SET", "INSTANCE"]);
|
|
3676
3860
|
function detectAnalysisScope(rootNode) {
|
|
@@ -3727,6 +3911,7 @@ var RuleEngine = class {
|
|
|
3727
3911
|
excludeNamePattern;
|
|
3728
3912
|
excludeNodeTypes;
|
|
3729
3913
|
acknowledgments;
|
|
3914
|
+
acknowledgmentsByKey;
|
|
3730
3915
|
scopeOverride;
|
|
3731
3916
|
constructor(options = {}) {
|
|
3732
3917
|
this.configs = options.configs ?? RULE_CONFIGS;
|
|
@@ -3735,10 +3920,15 @@ var RuleEngine = class {
|
|
|
3735
3920
|
this.targetNodeId = options.targetNodeId;
|
|
3736
3921
|
this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
|
|
3737
3922
|
this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
|
|
3923
|
+
const ackList = options.acknowledgments ?? [];
|
|
3738
3924
|
this.acknowledgments = new Set(
|
|
3739
|
-
(
|
|
3740
|
-
|
|
3741
|
-
|
|
3925
|
+
ackList.map((a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`)
|
|
3926
|
+
);
|
|
3927
|
+
this.acknowledgmentsByKey = new Map(
|
|
3928
|
+
ackList.map((a) => [
|
|
3929
|
+
`${normalizeNodeId(a.nodeId)}::${a.ruleId}`,
|
|
3930
|
+
a
|
|
3931
|
+
])
|
|
3742
3932
|
);
|
|
3743
3933
|
this.scopeOverride = options.scope;
|
|
3744
3934
|
}
|
|
@@ -3822,6 +4012,7 @@ var RuleEngine = class {
|
|
|
3822
4012
|
if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
|
|
3823
4013
|
return;
|
|
3824
4014
|
}
|
|
4015
|
+
const acknowledgmentsByKey = this.acknowledgmentsByKey;
|
|
3825
4016
|
const context = {
|
|
3826
4017
|
file,
|
|
3827
4018
|
parent,
|
|
@@ -3833,7 +4024,8 @@ var RuleEngine = class {
|
|
|
3833
4024
|
siblings,
|
|
3834
4025
|
analysisState,
|
|
3835
4026
|
scope,
|
|
3836
|
-
rootNodeType
|
|
4027
|
+
rootNodeType,
|
|
4028
|
+
findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
|
|
3837
4029
|
};
|
|
3838
4030
|
for (const rule of rules) {
|
|
3839
4031
|
const ruleId = rule.definition.id;
|
|
@@ -4317,7 +4509,7 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
4317
4509
|
}
|
|
4318
4510
|
|
|
4319
4511
|
// package.json
|
|
4320
|
-
var version2 = "0.12.
|
|
4512
|
+
var version2 = "0.12.1";
|
|
4321
4513
|
|
|
4322
4514
|
// src/core/engine/scoring.ts
|
|
4323
4515
|
var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
|
|
@@ -4509,6 +4701,19 @@ function formatScoreSummary(report) {
|
|
|
4509
4701
|
}
|
|
4510
4702
|
return lines.join("\n");
|
|
4511
4703
|
}
|
|
4704
|
+
function formatCodeConnectCoverageLine(coverage) {
|
|
4705
|
+
const { mapped, total } = coverage;
|
|
4706
|
+
const pct = total === 0 ? 0 : Math.round(mapped / total * 100);
|
|
4707
|
+
return `Code Connect coverage: ${mapped}/${total} components (${pct}%) mapped`;
|
|
4708
|
+
}
|
|
4709
|
+
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.";
|
|
4710
|
+
function formatRoundtripOptOutHintLine(issues, acknowledgmentsProvided) {
|
|
4711
|
+
const hasUnmapped = issues.some(
|
|
4712
|
+
(issue) => issue.violation.ruleId === "unmapped-component"
|
|
4713
|
+
);
|
|
4714
|
+
if (!hasUnmapped) return null;
|
|
4715
|
+
return ROUNDTRIP_OPT_OUT_HINT;
|
|
4716
|
+
}
|
|
4512
4717
|
function buildResultJson(fileName, result, scores, options) {
|
|
4513
4718
|
const issuesByRule = {};
|
|
4514
4719
|
for (const issue of result.issues) {
|
|
@@ -4538,6 +4743,14 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
4538
4743
|
...issue.acknowledged === true ? { acknowledged: true } : {}
|
|
4539
4744
|
};
|
|
4540
4745
|
});
|
|
4746
|
+
const optOutHint = options?.roundtripOptOutHintEligible ? formatRoundtripOptOutHintLine(result.issues) : null;
|
|
4747
|
+
const summaryParts = [formatScoreSummary(scores)];
|
|
4748
|
+
if (options?.codeConnectCoverage) {
|
|
4749
|
+
summaryParts.push(formatCodeConnectCoverageLine(options.codeConnectCoverage));
|
|
4750
|
+
}
|
|
4751
|
+
if (optOutHint) {
|
|
4752
|
+
summaryParts.push(optOutHint);
|
|
4753
|
+
}
|
|
4541
4754
|
const json = {
|
|
4542
4755
|
version: version2,
|
|
4543
4756
|
analyzedAt: result.analyzedAt,
|
|
@@ -4557,13 +4770,31 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
4557
4770
|
},
|
|
4558
4771
|
issuesByRule,
|
|
4559
4772
|
issues,
|
|
4560
|
-
summary:
|
|
4773
|
+
summary: summaryParts.join("\n\n")
|
|
4561
4774
|
};
|
|
4775
|
+
if (options?.codeConnectCoverage) {
|
|
4776
|
+
json["codeConnectCoverage"] = options.codeConnectCoverage;
|
|
4777
|
+
}
|
|
4778
|
+
if (optOutHint) {
|
|
4779
|
+
json["roundtripOptOutHint"] = optOutHint;
|
|
4780
|
+
}
|
|
4562
4781
|
if (result.failedRules.length > 0) {
|
|
4563
4782
|
json["failedRules"] = result.failedRules;
|
|
4564
4783
|
}
|
|
4565
4784
|
return json;
|
|
4566
4785
|
}
|
|
4786
|
+
|
|
4787
|
+
// src/core/rules/component/code-connect-coverage.ts
|
|
4788
|
+
function computeCodeConnectCoverage(components, cwd = process.cwd()) {
|
|
4789
|
+
const result = parseCodeConnectMappings(cwd);
|
|
4790
|
+
if (result.skipReason === "no-config") return void 0;
|
|
4791
|
+
const componentNodeIds = Object.keys(components);
|
|
4792
|
+
let mapped = 0;
|
|
4793
|
+
for (const nodeId of componentNodeIds) {
|
|
4794
|
+
if (result.mappedNodeIds.has(nodeId)) mapped++;
|
|
4795
|
+
}
|
|
4796
|
+
return { mapped, total: componentNodeIds.length };
|
|
4797
|
+
}
|
|
4567
4798
|
function isFigmaUrl2(input) {
|
|
4568
4799
|
return input.includes("figma.com/");
|
|
4569
4800
|
}
|
|
@@ -5922,8 +6153,17 @@ Analyzing: ${file.name}`);
|
|
|
5922
6153
|
const result = analyzeFile(file, analyzeOptions);
|
|
5923
6154
|
log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
|
|
5924
6155
|
const scores = calculateScores(result, configs);
|
|
6156
|
+
const coverage = computeCodeConnectCoverage(file.components);
|
|
6157
|
+
const optOutHintEligible = acknowledgments === void 0;
|
|
6158
|
+
const optOutHint = optOutHintEligible ? formatRoundtripOptOutHintLine(result.issues, false) : null;
|
|
5925
6159
|
if (options.json) {
|
|
5926
|
-
console.log(JSON.stringify(buildResultJson(file.name, result, scores, {
|
|
6160
|
+
console.log(JSON.stringify(buildResultJson(file.name, result, scores, {
|
|
6161
|
+
fileKey: file.fileKey,
|
|
6162
|
+
designKey: computeDesignKey(input),
|
|
6163
|
+
...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {},
|
|
6164
|
+
...coverage ? { codeConnectCoverage: coverage } : {},
|
|
6165
|
+
roundtripOptOutHintEligible: optOutHintEligible
|
|
6166
|
+
}), null, 2));
|
|
5927
6167
|
if (scores.overall.grade === "F") {
|
|
5928
6168
|
process.exitCode = 1;
|
|
5929
6169
|
}
|
|
@@ -5931,6 +6171,14 @@ Analyzing: ${file.name}`);
|
|
|
5931
6171
|
}
|
|
5932
6172
|
console.log("\n" + "=".repeat(50));
|
|
5933
6173
|
console.log(formatScoreSummary(scores));
|
|
6174
|
+
if (coverage) {
|
|
6175
|
+
console.log("");
|
|
6176
|
+
console.log(formatCodeConnectCoverageLine(coverage));
|
|
6177
|
+
}
|
|
6178
|
+
if (optOutHint) {
|
|
6179
|
+
console.log("");
|
|
6180
|
+
console.log(optOutHint);
|
|
6181
|
+
}
|
|
5934
6182
|
console.log("=".repeat(50));
|
|
5935
6183
|
const now = /* @__PURE__ */ new Date();
|
|
5936
6184
|
const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}`;
|
|
@@ -6785,8 +7033,8 @@ function renderUpsertedFile(args) {
|
|
|
6785
7033
|
}
|
|
6786
7034
|
let working = currentContent;
|
|
6787
7035
|
if (state === "missing-heading") {
|
|
6788
|
-
const
|
|
6789
|
-
working = `${working}${
|
|
7036
|
+
const sep2 = working.endsWith("\n\n") ? "" : working.endsWith("\n") ? "\n" : "\n\n";
|
|
7037
|
+
working = `${working}${sep2}${COLLECTED_GOTCHAS_HEADING}
|
|
6790
7038
|
`;
|
|
6791
7039
|
}
|
|
6792
7040
|
const plan = findOrAppendSection(working, designKey);
|
|
@@ -7645,6 +7893,9 @@ function registerConfig(cli2) {
|
|
|
7645
7893
|
}
|
|
7646
7894
|
});
|
|
7647
7895
|
}
|
|
7896
|
+
|
|
7897
|
+
// src/cli/commands/doctor.ts
|
|
7898
|
+
init_figma_client();
|
|
7648
7899
|
var CODE_CONNECT_PKG = "@figma/code-connect";
|
|
7649
7900
|
var CODE_CONNECT_DOCS = "https://www.figma.com/code-connect-docs/";
|
|
7650
7901
|
function readPackageJson(cwd) {
|
|
@@ -7692,10 +7943,85 @@ function runCodeConnectChecks(cwd) {
|
|
|
7692
7943
|
}
|
|
7693
7944
|
return results;
|
|
7694
7945
|
}
|
|
7946
|
+
var PUBLISH_CHECK_NAME = "Figma component published in a library";
|
|
7947
|
+
async function runFigmaPublishCheck(input) {
|
|
7948
|
+
const { figmaUrl, token, fetchPublishedComponents } = input;
|
|
7949
|
+
let parsed;
|
|
7950
|
+
try {
|
|
7951
|
+
parsed = parseFigmaUrl(figmaUrl);
|
|
7952
|
+
} catch (err) {
|
|
7953
|
+
const message = err instanceof FigmaUrlParseError ? err.message : String(err);
|
|
7954
|
+
return {
|
|
7955
|
+
name: PUBLISH_CHECK_NAME,
|
|
7956
|
+
pass: false,
|
|
7957
|
+
inconclusive: true,
|
|
7958
|
+
detail: `could not parse URL: ${message}`,
|
|
7959
|
+
remediation: "Pass a valid Figma design URL (figma.com/design/<file>?node-id=<id>)."
|
|
7960
|
+
};
|
|
7961
|
+
}
|
|
7962
|
+
if (!parsed.nodeId) {
|
|
7963
|
+
return {
|
|
7964
|
+
name: PUBLISH_CHECK_NAME,
|
|
7965
|
+
pass: false,
|
|
7966
|
+
inconclusive: true,
|
|
7967
|
+
detail: "URL is missing a node-id",
|
|
7968
|
+
remediation: "Code Connect mapping is per-component \u2014 invoke with a URL that targets a specific node (?node-id=\u2026)."
|
|
7969
|
+
};
|
|
7970
|
+
}
|
|
7971
|
+
if (!token) {
|
|
7972
|
+
return {
|
|
7973
|
+
name: PUBLISH_CHECK_NAME,
|
|
7974
|
+
pass: false,
|
|
7975
|
+
inconclusive: true,
|
|
7976
|
+
detail: "FIGMA_TOKEN not configured \u2014 skipping publish-status check",
|
|
7977
|
+
remediation: "Set FIGMA_TOKEN (env var) or run `canicode config set-token` so doctor can verify this prereq inline."
|
|
7978
|
+
};
|
|
7979
|
+
}
|
|
7980
|
+
if (!fetchPublishedComponents) {
|
|
7981
|
+
return {
|
|
7982
|
+
name: PUBLISH_CHECK_NAME,
|
|
7983
|
+
pass: false,
|
|
7984
|
+
inconclusive: true,
|
|
7985
|
+
detail: "no fetcher wired",
|
|
7986
|
+
remediation: "internal: doctor was called without a Figma client"
|
|
7987
|
+
};
|
|
7988
|
+
}
|
|
7989
|
+
let components;
|
|
7990
|
+
try {
|
|
7991
|
+
components = await fetchPublishedComponents(parsed.fileKey);
|
|
7992
|
+
} catch (err) {
|
|
7993
|
+
const status = err instanceof FigmaClientError ? err.statusCode : void 0;
|
|
7994
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7995
|
+
return {
|
|
7996
|
+
name: PUBLISH_CHECK_NAME,
|
|
7997
|
+
pass: false,
|
|
7998
|
+
inconclusive: true,
|
|
7999
|
+
detail: `Figma API call failed${status ? ` (HTTP ${status})` : ""}: ${message}`,
|
|
8000
|
+
remediation: "Step 7d will rely on the API as the authority; if your token / network is OK, the canicode-roundtrip step itself will surface the publish error inline."
|
|
8001
|
+
};
|
|
8002
|
+
}
|
|
8003
|
+
const canonicalNodeId = parsed.nodeId.replace(/-/g, ":");
|
|
8004
|
+
const match = components.find(
|
|
8005
|
+
(c) => c.node_id === canonicalNodeId || c.node_id === parsed.nodeId
|
|
8006
|
+
);
|
|
8007
|
+
if (match) {
|
|
8008
|
+
return {
|
|
8009
|
+
name: PUBLISH_CHECK_NAME,
|
|
8010
|
+
pass: true,
|
|
8011
|
+
detail: `${match.name} (${match.node_id})`
|
|
8012
|
+
};
|
|
8013
|
+
}
|
|
8014
|
+
return {
|
|
8015
|
+
name: PUBLISH_CHECK_NAME,
|
|
8016
|
+
pass: false,
|
|
8017
|
+
detail: `node ${canonicalNodeId} is not in the published-components list for file ${parsed.fileKey}`,
|
|
8018
|
+
remediation: "Open the file in Figma \u2192 Assets panel \u2192 Publish library and include this component. Without publishing, `add_code_connect_map` fails with 'Published component not found.'"
|
|
8019
|
+
};
|
|
8020
|
+
}
|
|
7695
8021
|
function formatDoctorReport(results) {
|
|
7696
8022
|
const lines = ["Code Connect"];
|
|
7697
8023
|
for (const result of results) {
|
|
7698
|
-
const icon = result.pass ? "\u2705" : "\u274C";
|
|
8024
|
+
const icon = result.pass ? "\u2705" : result.inconclusive ? "\u26A0\uFE0F" : "\u274C";
|
|
7699
8025
|
const detail = result.detail ? ` (${result.detail})` : "";
|
|
7700
8026
|
lines.push(` ${icon} ${result.name}${detail}`);
|
|
7701
8027
|
if (!result.pass && result.remediation) {
|
|
@@ -7703,22 +8029,43 @@ function formatDoctorReport(results) {
|
|
|
7703
8029
|
}
|
|
7704
8030
|
}
|
|
7705
8031
|
lines.push("");
|
|
7706
|
-
const
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
8032
|
+
const blocking = results.filter((r) => !r.pass && !r.inconclusive).length;
|
|
8033
|
+
const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
|
|
8034
|
+
if (blocking === 0 && inconclusive === 0) {
|
|
8035
|
+
lines.push("All checks passed.");
|
|
8036
|
+
} else if (blocking === 0) {
|
|
8037
|
+
lines.push(
|
|
8038
|
+
"Blocking checks passed; some checks were skipped (\u26A0\uFE0F) and could not be verified."
|
|
8039
|
+
);
|
|
8040
|
+
} else {
|
|
8041
|
+
lines.push("Some checks failed. Fix the items above before running the Code Connect flow.");
|
|
8042
|
+
}
|
|
7710
8043
|
return lines.join("\n");
|
|
7711
8044
|
}
|
|
7712
8045
|
function registerDoctor(cli2) {
|
|
7713
8046
|
cli2.command(
|
|
7714
8047
|
"doctor",
|
|
7715
8048
|
"Diagnose Code Connect prerequisites (`@figma/code-connect`, `figma.config.json`)"
|
|
7716
|
-
).
|
|
8049
|
+
).option(
|
|
8050
|
+
"--figma-url <url>",
|
|
8051
|
+
"Optionally check that the target Figma component is published in a library (requires FIGMA_TOKEN)"
|
|
8052
|
+
).action(async (options) => {
|
|
7717
8053
|
const cwd = process.cwd();
|
|
7718
8054
|
const results = runCodeConnectChecks(cwd);
|
|
8055
|
+
if (options.figmaUrl) {
|
|
8056
|
+
const token = getFigmaToken();
|
|
8057
|
+
const client = token ? new FigmaClient({ token }) : void 0;
|
|
8058
|
+
const publishCheck = await runFigmaPublishCheck({
|
|
8059
|
+
figmaUrl: options.figmaUrl,
|
|
8060
|
+
token,
|
|
8061
|
+
fetchPublishedComponents: client ? (fileKey) => client.getPublishedComponents(fileKey) : void 0
|
|
8062
|
+
});
|
|
8063
|
+
results.push(publishCheck);
|
|
8064
|
+
}
|
|
7719
8065
|
console.log(formatDoctorReport(results));
|
|
7720
8066
|
const passed = results.filter((r) => r.pass).length;
|
|
7721
|
-
const
|
|
8067
|
+
const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
|
|
8068
|
+
const failed = results.length - passed - inconclusive;
|
|
7722
8069
|
trackEvent(EVENTS.CLI_DOCTOR, {
|
|
7723
8070
|
passed,
|
|
7724
8071
|
failed,
|
|
@@ -10856,8 +11203,8 @@ ${msg}`);
|
|
|
10856
11203
|
if (vectorDir && existsSync(vectorDir)) {
|
|
10857
11204
|
const vecOutputDir = resolve(outputDir, "vectors");
|
|
10858
11205
|
mkdirSync(vecOutputDir, { recursive: true });
|
|
10859
|
-
const { readdirSync:
|
|
10860
|
-
const vecFiles =
|
|
11206
|
+
const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
|
|
11207
|
+
const vecFiles = readdirSync5(vectorDir).filter((f) => f.endsWith(".svg") || f === "mapping.json");
|
|
10861
11208
|
for (const f of vecFiles) {
|
|
10862
11209
|
copyFileSync3(resolve(vectorDir, f), resolve(vecOutputDir, f));
|
|
10863
11210
|
}
|
|
@@ -10867,8 +11214,8 @@ ${msg}`);
|
|
|
10867
11214
|
if (imageDir && existsSync(imageDir)) {
|
|
10868
11215
|
const imgOutputDir = resolve(outputDir, "images");
|
|
10869
11216
|
mkdirSync(imgOutputDir, { recursive: true });
|
|
10870
|
-
const { readdirSync:
|
|
10871
|
-
const imgFiles =
|
|
11217
|
+
const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
|
|
11218
|
+
const imgFiles = readdirSync5(imageDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg") || f.endsWith(".json"));
|
|
10872
11219
|
for (const f of imgFiles) {
|
|
10873
11220
|
copyFileSync3(resolve(imageDir, f), resolve(imgOutputDir, f));
|
|
10874
11221
|
}
|