canicode 0.12.0 → 0.12.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/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,23 @@ 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";
3461
+ var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
3264
3462
  function codeConnectIsSetUp(context) {
3265
3463
  return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
3266
3464
  return existsSync(join(process.cwd(), "figma.config.json"));
3267
3465
  });
3268
3466
  }
3467
+ function codeConnectMappings(context) {
3468
+ return getAnalysisState(
3469
+ context,
3470
+ CODE_CONNECT_MAPPINGS_KEY,
3471
+ () => parseCodeConnectMappings(process.cwd())
3472
+ );
3473
+ }
3474
+ function seenMainIds(context) {
3475
+ return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());
3476
+ }
3269
3477
  var unmappedComponentDef = {
3270
3478
  id: "unmapped-component",
3271
3479
  name: "Unmapped Component",
@@ -3275,14 +3483,33 @@ var unmappedComponentDef = {
3275
3483
  fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
3276
3484
  };
3277
3485
  var unmappedComponentCheck = (node, context) => {
3278
- if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
3279
- if (isInsideInstance(context)) return null;
3280
3486
  if (!codeConnectIsSetUp(context)) return null;
3487
+ let mainId = null;
3488
+ let mainName = node.name;
3489
+ if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
3490
+ if (isInsideInstance(context)) return null;
3491
+ mainId = node.id;
3492
+ } else if (node.type === "INSTANCE" && node.componentId) {
3493
+ mainId = node.componentId;
3494
+ const meta = context.file.components[node.componentId];
3495
+ if (meta?.name) mainName = meta.name;
3496
+ } else {
3497
+ return null;
3498
+ }
3499
+ const seen = seenMainIds(context);
3500
+ if (seen.has(mainId)) return null;
3501
+ seen.add(mainId);
3502
+ const mappings = codeConnectMappings(context);
3503
+ if (mappings.mappedNodeIds.has(mainId)) return null;
3504
+ const ack = context.findAcknowledgment(mainId, unmappedComponentDef.id);
3505
+ if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
3506
+ return null;
3507
+ }
3281
3508
  return {
3282
3509
  ruleId: unmappedComponentDef.id,
3283
- nodeId: node.id,
3510
+ nodeId: mainId,
3284
3511
  nodePath: context.path.join(" > "),
3285
- ...unmappedComponentMsg(node.name)
3512
+ ...unmappedComponentMsg(mainName)
3286
3513
  };
3287
3514
  };
3288
3515
  defineRule({
@@ -3645,32 +3872,6 @@ defineRule({
3645
3872
  definition: missingPrototypeDef,
3646
3873
  check: missingPrototypeCheck
3647
3874
  });
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
3875
  var AnalysisScopeSchema = z.enum(["page", "component"]);
3675
3876
  var COMPONENT_SCOPE_ROOT_TYPES = /* @__PURE__ */ new Set(["COMPONENT", "COMPONENT_SET", "INSTANCE"]);
3676
3877
  function detectAnalysisScope(rootNode) {
@@ -3727,6 +3928,7 @@ var RuleEngine = class {
3727
3928
  excludeNamePattern;
3728
3929
  excludeNodeTypes;
3729
3930
  acknowledgments;
3931
+ acknowledgmentsByKey;
3730
3932
  scopeOverride;
3731
3933
  constructor(options = {}) {
3732
3934
  this.configs = options.configs ?? RULE_CONFIGS;
@@ -3735,10 +3937,15 @@ var RuleEngine = class {
3735
3937
  this.targetNodeId = options.targetNodeId;
3736
3938
  this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
3737
3939
  this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
3940
+ const ackList = options.acknowledgments ?? [];
3738
3941
  this.acknowledgments = new Set(
3739
- (options.acknowledgments ?? []).map(
3740
- (a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
3741
- )
3942
+ ackList.map((a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`)
3943
+ );
3944
+ this.acknowledgmentsByKey = new Map(
3945
+ ackList.map((a) => [
3946
+ `${normalizeNodeId(a.nodeId)}::${a.ruleId}`,
3947
+ a
3948
+ ])
3742
3949
  );
3743
3950
  this.scopeOverride = options.scope;
3744
3951
  }
@@ -3822,6 +4029,7 @@ var RuleEngine = class {
3822
4029
  if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
3823
4030
  return;
3824
4031
  }
4032
+ const acknowledgmentsByKey = this.acknowledgmentsByKey;
3825
4033
  const context = {
3826
4034
  file,
3827
4035
  parent,
@@ -3833,7 +4041,8 @@ var RuleEngine = class {
3833
4041
  siblings,
3834
4042
  analysisState,
3835
4043
  scope,
3836
- rootNodeType
4044
+ rootNodeType,
4045
+ findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
3837
4046
  };
3838
4047
  for (const rule of rules) {
3839
4048
  const ruleId = rule.definition.id;
@@ -4317,7 +4526,7 @@ function computeApplyContext(violation, instanceContext) {
4317
4526
  }
4318
4527
 
4319
4528
  // package.json
4320
- var version2 = "0.12.0";
4529
+ var version2 = "0.12.2";
4321
4530
 
4322
4531
  // src/core/engine/scoring.ts
4323
4532
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -4509,6 +4718,19 @@ function formatScoreSummary(report) {
4509
4718
  }
4510
4719
  return lines.join("\n");
4511
4720
  }
4721
+ function formatCodeConnectCoverageLine(coverage) {
4722
+ const { mapped, total } = coverage;
4723
+ const pct = total === 0 ? 0 : Math.round(mapped / total * 100);
4724
+ return `Code Connect coverage: ${mapped}/${total} components (${pct}%) mapped`;
4725
+ }
4726
+ 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.";
4727
+ function formatRoundtripOptOutHintLine(issues, acknowledgmentsProvided) {
4728
+ const hasUnmapped = issues.some(
4729
+ (issue) => issue.violation.ruleId === "unmapped-component"
4730
+ );
4731
+ if (!hasUnmapped) return null;
4732
+ return ROUNDTRIP_OPT_OUT_HINT;
4733
+ }
4512
4734
  function buildResultJson(fileName, result, scores, options) {
4513
4735
  const issuesByRule = {};
4514
4736
  for (const issue of result.issues) {
@@ -4538,6 +4760,14 @@ function buildResultJson(fileName, result, scores, options) {
4538
4760
  ...issue.acknowledged === true ? { acknowledged: true } : {}
4539
4761
  };
4540
4762
  });
4763
+ const optOutHint = options?.roundtripOptOutHintEligible ? formatRoundtripOptOutHintLine(result.issues) : null;
4764
+ const summaryParts = [formatScoreSummary(scores)];
4765
+ if (options?.codeConnectCoverage) {
4766
+ summaryParts.push(formatCodeConnectCoverageLine(options.codeConnectCoverage));
4767
+ }
4768
+ if (optOutHint) {
4769
+ summaryParts.push(optOutHint);
4770
+ }
4541
4771
  const json = {
4542
4772
  version: version2,
4543
4773
  analyzedAt: result.analyzedAt,
@@ -4557,13 +4787,31 @@ function buildResultJson(fileName, result, scores, options) {
4557
4787
  },
4558
4788
  issuesByRule,
4559
4789
  issues,
4560
- summary: formatScoreSummary(scores)
4790
+ summary: summaryParts.join("\n\n")
4561
4791
  };
4792
+ if (options?.codeConnectCoverage) {
4793
+ json["codeConnectCoverage"] = options.codeConnectCoverage;
4794
+ }
4795
+ if (optOutHint) {
4796
+ json["roundtripOptOutHint"] = optOutHint;
4797
+ }
4562
4798
  if (result.failedRules.length > 0) {
4563
4799
  json["failedRules"] = result.failedRules;
4564
4800
  }
4565
4801
  return json;
4566
4802
  }
4803
+
4804
+ // src/core/rules/component/code-connect-coverage.ts
4805
+ function computeCodeConnectCoverage(components, cwd = process.cwd()) {
4806
+ const result = parseCodeConnectMappings(cwd);
4807
+ if (result.skipReason === "no-config") return void 0;
4808
+ const componentNodeIds = Object.keys(components);
4809
+ let mapped = 0;
4810
+ for (const nodeId of componentNodeIds) {
4811
+ if (result.mappedNodeIds.has(nodeId)) mapped++;
4812
+ }
4813
+ return { mapped, total: componentNodeIds.length };
4814
+ }
4567
4815
  function isFigmaUrl2(input) {
4568
4816
  return input.includes("figma.com/");
4569
4817
  }
@@ -5922,8 +6170,17 @@ Analyzing: ${file.name}`);
5922
6170
  const result = analyzeFile(file, analyzeOptions);
5923
6171
  log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
5924
6172
  const scores = calculateScores(result, configs);
6173
+ const coverage = computeCodeConnectCoverage(file.components);
6174
+ const optOutHintEligible = acknowledgments === void 0;
6175
+ const optOutHint = optOutHintEligible ? formatRoundtripOptOutHintLine(result.issues, false) : null;
5925
6176
  if (options.json) {
5926
- console.log(JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input), ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {} }), null, 2));
6177
+ console.log(JSON.stringify(buildResultJson(file.name, result, scores, {
6178
+ fileKey: file.fileKey,
6179
+ designKey: computeDesignKey(input),
6180
+ ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {},
6181
+ ...coverage ? { codeConnectCoverage: coverage } : {},
6182
+ roundtripOptOutHintEligible: optOutHintEligible
6183
+ }), null, 2));
5927
6184
  if (scores.overall.grade === "F") {
5928
6185
  process.exitCode = 1;
5929
6186
  }
@@ -5931,6 +6188,14 @@ Analyzing: ${file.name}`);
5931
6188
  }
5932
6189
  console.log("\n" + "=".repeat(50));
5933
6190
  console.log(formatScoreSummary(scores));
6191
+ if (coverage) {
6192
+ console.log("");
6193
+ console.log(formatCodeConnectCoverageLine(coverage));
6194
+ }
6195
+ if (optOutHint) {
6196
+ console.log("");
6197
+ console.log(optOutHint);
6198
+ }
5934
6199
  console.log("=".repeat(50));
5935
6200
  const now = /* @__PURE__ */ new Date();
5936
6201
  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 +7050,8 @@ function renderUpsertedFile(args) {
6785
7050
  }
6786
7051
  let working = currentContent;
6787
7052
  if (state === "missing-heading") {
6788
- const sep = working.endsWith("\n\n") ? "" : working.endsWith("\n") ? "\n" : "\n\n";
6789
- working = `${working}${sep}${COLLECTED_GOTCHAS_HEADING}
7053
+ const sep2 = working.endsWith("\n\n") ? "" : working.endsWith("\n") ? "\n" : "\n\n";
7054
+ working = `${working}${sep2}${COLLECTED_GOTCHAS_HEADING}
6790
7055
  `;
6791
7056
  }
6792
7057
  const plan = findOrAppendSection(working, designKey);
@@ -7645,6 +7910,9 @@ function registerConfig(cli2) {
7645
7910
  }
7646
7911
  });
7647
7912
  }
7913
+
7914
+ // src/cli/commands/doctor.ts
7915
+ init_figma_client();
7648
7916
  var CODE_CONNECT_PKG = "@figma/code-connect";
7649
7917
  var CODE_CONNECT_DOCS = "https://www.figma.com/code-connect-docs/";
7650
7918
  function readPackageJson(cwd) {
@@ -7692,10 +7960,102 @@ function runCodeConnectChecks(cwd) {
7692
7960
  }
7693
7961
  return results;
7694
7962
  }
7963
+ var PUBLISH_CHECK_NAME = "Figma component published in a library";
7964
+ async function runFigmaPublishCheck(input) {
7965
+ const { figmaUrl, token, fetchPublishedComponents, fetchNodeType } = input;
7966
+ let parsed;
7967
+ try {
7968
+ parsed = parseFigmaUrl(figmaUrl);
7969
+ } catch (err) {
7970
+ const message = err instanceof FigmaUrlParseError ? err.message : String(err);
7971
+ return {
7972
+ name: PUBLISH_CHECK_NAME,
7973
+ pass: false,
7974
+ inconclusive: true,
7975
+ detail: `could not parse URL: ${message}`,
7976
+ remediation: "Pass a valid Figma design URL (figma.com/design/<file>?node-id=<id>)."
7977
+ };
7978
+ }
7979
+ if (!parsed.nodeId) {
7980
+ return {
7981
+ name: PUBLISH_CHECK_NAME,
7982
+ pass: false,
7983
+ inconclusive: true,
7984
+ detail: "URL is missing a node-id",
7985
+ remediation: "Code Connect mapping is per-component \u2014 invoke with a URL that targets a specific node (?node-id=\u2026)."
7986
+ };
7987
+ }
7988
+ if (!token) {
7989
+ return {
7990
+ name: PUBLISH_CHECK_NAME,
7991
+ pass: false,
7992
+ inconclusive: true,
7993
+ detail: "FIGMA_TOKEN not configured \u2014 skipping publish-status check",
7994
+ remediation: "Set FIGMA_TOKEN (env var) or run `canicode config set-token` so doctor can verify this prereq inline."
7995
+ };
7996
+ }
7997
+ if (!fetchPublishedComponents) {
7998
+ return {
7999
+ name: PUBLISH_CHECK_NAME,
8000
+ pass: false,
8001
+ inconclusive: true,
8002
+ detail: "no fetcher wired",
8003
+ remediation: "internal: doctor was called without a Figma client"
8004
+ };
8005
+ }
8006
+ let components;
8007
+ try {
8008
+ components = await fetchPublishedComponents(parsed.fileKey);
8009
+ } catch (err) {
8010
+ const status = err instanceof FigmaClientError ? err.statusCode : void 0;
8011
+ const message = err instanceof Error ? err.message : String(err);
8012
+ return {
8013
+ name: PUBLISH_CHECK_NAME,
8014
+ pass: false,
8015
+ inconclusive: true,
8016
+ detail: `Figma API call failed${status ? ` (HTTP ${status})` : ""}: ${message}`,
8017
+ 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."
8018
+ };
8019
+ }
8020
+ const canonicalNodeId = parsed.nodeId.replace(/-/g, ":");
8021
+ const match = components.find(
8022
+ (c) => c.node_id === canonicalNodeId || c.node_id === parsed.nodeId
8023
+ );
8024
+ if (match) {
8025
+ return {
8026
+ name: PUBLISH_CHECK_NAME,
8027
+ pass: true,
8028
+ detail: `${match.name} (${match.node_id})`
8029
+ };
8030
+ }
8031
+ if (fetchNodeType) {
8032
+ let nodeType;
8033
+ try {
8034
+ nodeType = await fetchNodeType(parsed.fileKey, canonicalNodeId);
8035
+ } catch {
8036
+ nodeType = void 0;
8037
+ }
8038
+ if (nodeType && nodeType !== "COMPONENT" && nodeType !== "COMPONENT_SET") {
8039
+ return {
8040
+ name: PUBLISH_CHECK_NAME,
8041
+ pass: false,
8042
+ inconclusive: true,
8043
+ detail: `node ${canonicalNodeId} is type ${nodeType} \u2014 Code Connect mapping is per-component`,
8044
+ remediation: "Step 7 (Code Connect close-out) skips on screen-level scope anyway. To verify a specific component, re-invoke doctor with that component's URL."
8045
+ };
8046
+ }
8047
+ }
8048
+ return {
8049
+ name: PUBLISH_CHECK_NAME,
8050
+ pass: false,
8051
+ detail: `node ${canonicalNodeId} is not in the published-components list for file ${parsed.fileKey}`,
8052
+ 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.'"
8053
+ };
8054
+ }
7695
8055
  function formatDoctorReport(results) {
7696
8056
  const lines = ["Code Connect"];
7697
8057
  for (const result of results) {
7698
- const icon = result.pass ? "\u2705" : "\u274C";
8058
+ const icon = result.pass ? "\u2705" : result.inconclusive ? "\u26A0\uFE0F" : "\u274C";
7699
8059
  const detail = result.detail ? ` (${result.detail})` : "";
7700
8060
  lines.push(` ${icon} ${result.name}${detail}`);
7701
8061
  if (!result.pass && result.remediation) {
@@ -7703,22 +8063,47 @@ function formatDoctorReport(results) {
7703
8063
  }
7704
8064
  }
7705
8065
  lines.push("");
7706
- const allPass = results.every((r) => r.pass);
7707
- lines.push(
7708
- allPass ? "All checks passed." : "Some checks failed. Fix the items above before running the Code Connect flow."
7709
- );
8066
+ const blocking = results.filter((r) => !r.pass && !r.inconclusive).length;
8067
+ const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
8068
+ if (blocking === 0 && inconclusive === 0) {
8069
+ lines.push("All checks passed.");
8070
+ } else if (blocking === 0) {
8071
+ lines.push(
8072
+ "Blocking checks passed; some checks were skipped (\u26A0\uFE0F) and could not be verified."
8073
+ );
8074
+ } else {
8075
+ lines.push("Some checks failed. Fix the items above before running the Code Connect flow.");
8076
+ }
7710
8077
  return lines.join("\n");
7711
8078
  }
7712
8079
  function registerDoctor(cli2) {
7713
8080
  cli2.command(
7714
8081
  "doctor",
7715
8082
  "Diagnose Code Connect prerequisites (`@figma/code-connect`, `figma.config.json`)"
7716
- ).action(() => {
8083
+ ).option(
8084
+ "--figma-url <url>",
8085
+ "Optionally check that the target Figma component is published in a library (requires FIGMA_TOKEN)"
8086
+ ).action(async (options) => {
7717
8087
  const cwd = process.cwd();
7718
8088
  const results = runCodeConnectChecks(cwd);
8089
+ if (options.figmaUrl) {
8090
+ const token = getFigmaToken();
8091
+ const client = token ? new FigmaClient({ token }) : void 0;
8092
+ const publishCheck = await runFigmaPublishCheck({
8093
+ figmaUrl: options.figmaUrl,
8094
+ token,
8095
+ fetchPublishedComponents: client ? (fileKey) => client.getPublishedComponents(fileKey) : void 0,
8096
+ fetchNodeType: client ? async (fileKey, nodeId) => {
8097
+ const response = await client.getFileNodes(fileKey, [nodeId]);
8098
+ return response.nodes[nodeId]?.document.type;
8099
+ } : void 0
8100
+ });
8101
+ results.push(publishCheck);
8102
+ }
7719
8103
  console.log(formatDoctorReport(results));
7720
8104
  const passed = results.filter((r) => r.pass).length;
7721
- const failed = results.length - passed;
8105
+ const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
8106
+ const failed = results.length - passed - inconclusive;
7722
8107
  trackEvent(EVENTS.CLI_DOCTOR, {
7723
8108
  passed,
7724
8109
  failed,
@@ -10856,8 +11241,8 @@ ${msg}`);
10856
11241
  if (vectorDir && existsSync(vectorDir)) {
10857
11242
  const vecOutputDir = resolve(outputDir, "vectors");
10858
11243
  mkdirSync(vecOutputDir, { recursive: true });
10859
- const { readdirSync: readdirSync4, copyFileSync: copyFileSync3 } = await import('fs');
10860
- const vecFiles = readdirSync4(vectorDir).filter((f) => f.endsWith(".svg") || f === "mapping.json");
11244
+ const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
11245
+ const vecFiles = readdirSync5(vectorDir).filter((f) => f.endsWith(".svg") || f === "mapping.json");
10861
11246
  for (const f of vecFiles) {
10862
11247
  copyFileSync3(resolve(vectorDir, f), resolve(vecOutputDir, f));
10863
11248
  }
@@ -10867,8 +11252,8 @@ ${msg}`);
10867
11252
  if (imageDir && existsSync(imageDir)) {
10868
11253
  const imgOutputDir = resolve(outputDir, "images");
10869
11254
  mkdirSync(imgOutputDir, { recursive: true });
10870
- const { readdirSync: readdirSync4, copyFileSync: copyFileSync3 } = await import('fs');
10871
- const imgFiles = readdirSync4(imageDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg") || f.endsWith(".json"));
11255
+ const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
11256
+ const imgFiles = readdirSync5(imageDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg") || f.endsWith(".json"));
10872
11257
  for (const f of imgFiles) {
10873
11258
  copyFileSync3(resolve(imageDir, f), resolve(imgOutputDir, f));
10874
11259
  }