figma-cache-toolchain 2.0.4 → 2.0.7

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.
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Import MCP raw evidence files into figma-cache node directory and generate a manifest.
6
+ *
7
+ * This is the toolchain-friendly way to implement: "call MCP once -> cache evidence".
8
+ *
9
+ * Usage:
10
+ * node scripts/import-mcp-raw-evidence.cjs \
11
+ * --cacheKey=<fileKey#nodeId> \
12
+ * --design-context=<path/to/get_design_context.txt> \
13
+ * --metadata=<path/to/get_metadata.txt> \
14
+ * --variable-defs=<path/to/get_variable_defs.json>
15
+ *
16
+ * Notes:
17
+ * - Writes to: figma-cache/files/<fileKey>/nodes/<safeNodeId>/mcp-raw/
18
+ * - Generates: mcp-raw-manifest.json with sha256 + byte sizes
19
+ */
20
+
21
+ const crypto = require("crypto");
22
+ const fs = require("fs");
23
+ const path = require("path");
24
+
25
+ function sha256Utf8(text) {
26
+ return crypto.createHash("sha256").update(String(text || ""), "utf8").digest("hex");
27
+ }
28
+
29
+ function sizeUtf8(text) {
30
+ return Buffer.byteLength(String(text || ""), "utf8");
31
+ }
32
+
33
+ function readUtf8(absPath) {
34
+ return fs.readFileSync(absPath, "utf8");
35
+ }
36
+
37
+ function resolveAbs(p) {
38
+ const v = String(p || "").trim();
39
+ if (!v) return "";
40
+ return path.isAbsolute(v) ? v : path.join(process.cwd(), v);
41
+ }
42
+
43
+ function normalizeNodeId(input) {
44
+ const value = String(input || "").trim();
45
+ if (!value) return "";
46
+ return value.includes(":") ? value : value.replace(/-/g, ":");
47
+ }
48
+
49
+ function parseArgs(argv) {
50
+ const out = {
51
+ cacheKey: "",
52
+ designContext: "",
53
+ metadata: "",
54
+ variableDefs: "",
55
+ };
56
+ argv.slice(2).forEach((arg) => {
57
+ if (arg.startsWith("--cacheKey=")) out.cacheKey = arg.split("=").slice(1).join("=").trim();
58
+ if (arg.startsWith("--design-context="))
59
+ out.designContext = arg.split("=").slice(1).join("=").trim();
60
+ if (arg.startsWith("--metadata=")) out.metadata = arg.split("=").slice(1).join("=").trim();
61
+ if (arg.startsWith("--variable-defs="))
62
+ out.variableDefs = arg.split("=").slice(1).join("=").trim();
63
+ });
64
+ return out;
65
+ }
66
+
67
+ function main() {
68
+ const args = parseArgs(process.argv);
69
+ if (!args.cacheKey || !args.designContext || !args.metadata || !args.variableDefs) {
70
+ console.error(
71
+ "Usage: node scripts/import-mcp-raw-evidence.cjs --cacheKey=<fileKey#nodeId> --design-context=<txt> --metadata=<txt> --variable-defs=<json>"
72
+ );
73
+ process.exit(2);
74
+ }
75
+
76
+ const cacheKey = String(args.cacheKey).trim();
77
+ const [fileKey, nodeIdRaw] = cacheKey.split("#");
78
+ const nodeId = normalizeNodeId(nodeIdRaw);
79
+ if (!fileKey || !nodeId) {
80
+ console.error(`[import-mcp-raw-evidence] invalid cacheKey: ${cacheKey}`);
81
+ process.exit(2);
82
+ }
83
+
84
+ const safeNodeDir = nodeId.replace(/:/g, "-");
85
+ const mcpRawDir = path.join(
86
+ process.cwd(),
87
+ "figma-cache",
88
+ "files",
89
+ fileKey,
90
+ "nodes",
91
+ safeNodeDir,
92
+ "mcp-raw"
93
+ );
94
+ fs.mkdirSync(mcpRawDir, { recursive: true });
95
+
96
+ const srcDesign = resolveAbs(args.designContext);
97
+ const srcMeta = resolveAbs(args.metadata);
98
+ const srcVars = resolveAbs(args.variableDefs);
99
+ if (!fs.existsSync(srcDesign) || !fs.existsSync(srcMeta) || !fs.existsSync(srcVars)) {
100
+ console.error("[import-mcp-raw-evidence] missing input file(s)");
101
+ process.exit(2);
102
+ }
103
+
104
+ const files = {
105
+ get_design_context: "mcp-raw-get-design-context.txt",
106
+ get_metadata: "mcp-raw-get-metadata.txt",
107
+ get_variable_defs: "mcp-raw-get-variable-defs.json",
108
+ };
109
+
110
+ const contents = {
111
+ get_design_context: readUtf8(srcDesign),
112
+ get_metadata: readUtf8(srcMeta),
113
+ get_variable_defs: readUtf8(srcVars),
114
+ };
115
+
116
+ fs.writeFileSync(path.join(mcpRawDir, files.get_design_context), contents.get_design_context, "utf8");
117
+ fs.writeFileSync(path.join(mcpRawDir, files.get_metadata), contents.get_metadata, "utf8");
118
+ fs.writeFileSync(path.join(mcpRawDir, files.get_variable_defs), contents.get_variable_defs, "utf8");
119
+
120
+ const manifest = {
121
+ mcpServer: "plugin-figma-figma",
122
+ fileKey,
123
+ nodeId,
124
+ files,
125
+ fileHashes: Object.fromEntries(Object.entries(contents).map(([k, v]) => [k, sha256Utf8(v)])),
126
+ fileSizes: Object.fromEntries(Object.entries(contents).map(([k, v]) => [k, sizeUtf8(v)])),
127
+ };
128
+
129
+ fs.writeFileSync(
130
+ path.join(mcpRawDir, "mcp-raw-manifest.json"),
131
+ `${JSON.stringify(manifest, null, 2)}\n`,
132
+ "utf8"
133
+ );
134
+
135
+ console.log(
136
+ `[import-mcp-raw-evidence] ok -> ${path.join(mcpRawDir, "mcp-raw-manifest.json")}`
137
+ );
138
+ }
139
+
140
+ main();
141
+
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Merge figma-geometry-metrics.json into raw.json as layoutMetrics[].
6
+ *
7
+ * Geometry file shape:
8
+ * {
9
+ * "version": 1,
10
+ * "source": "figma_plugin_absoluteBoundingBox",
11
+ * "metrics": [ { "id": "...", "kind": "spacer_between_nodes_y", "fromNodeId": "...", "toNodeId": "...", "spacerPx": 26, ... } ]
12
+ * }
13
+ *
14
+ * Usage:
15
+ * node scripts/merge-figma-geometry-metrics.cjs --raw=<raw.json> --geometry=<figma-geometry-metrics.json>
16
+ */
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+ const { mergeLayoutMetricsFromGeometry } = require("../figma-cache/js/raw-derivatives");
21
+
22
+ function safeReadJson(absPath) {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(absPath, "utf8"));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function writeJson(absPath, value) {
31
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
32
+ fs.writeFileSync(absPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
33
+ }
34
+
35
+ function parseArgs(argv) {
36
+ const out = { raw: "", geometry: "" };
37
+ argv.slice(2).forEach((arg) => {
38
+ if (arg.startsWith("--raw=")) out.raw = arg.split("=").slice(1).join("=").trim();
39
+ if (arg.startsWith("--geometry=")) out.geometry = arg.split("=").slice(1).join("=").trim();
40
+ });
41
+ return out;
42
+ }
43
+
44
+ function main() {
45
+ const args = parseArgs(process.argv);
46
+ const rawAbs = path.isAbsolute(args.raw) ? args.raw : path.join(process.cwd(), args.raw);
47
+ const geoAbs = path.isAbsolute(args.geometry) ? args.geometry : path.join(process.cwd(), args.geometry);
48
+ if (!args.raw || !args.geometry) {
49
+ console.error(
50
+ "Usage: node scripts/merge-figma-geometry-metrics.cjs --raw=<raw.json> --geometry=<figma-geometry-metrics.json>"
51
+ );
52
+ process.exit(2);
53
+ }
54
+ if (!fs.existsSync(rawAbs)) {
55
+ console.error(`[merge-figma-geometry-metrics] raw not found: ${rawAbs}`);
56
+ process.exit(2);
57
+ }
58
+ if (!fs.existsSync(geoAbs)) {
59
+ console.error(`[merge-figma-geometry-metrics] geometry not found: ${geoAbs}`);
60
+ process.exit(2);
61
+ }
62
+
63
+ const raw = safeReadJson(rawAbs);
64
+ const geo = safeReadJson(geoAbs);
65
+ if (!raw || typeof raw !== "object") {
66
+ console.error(`[merge-figma-geometry-metrics] invalid raw: ${rawAbs}`);
67
+ process.exit(2);
68
+ }
69
+ if (!geo || typeof geo !== "object" || !Array.isArray(geo.metrics)) {
70
+ console.error(`[merge-figma-geometry-metrics] invalid geometry (need .metrics[]): ${geoAbs}`);
71
+ process.exit(2);
72
+ }
73
+
74
+ mergeLayoutMetricsFromGeometry(raw, geo);
75
+ writeJson(rawAbs, raw);
76
+ console.log(
77
+ `[merge-figma-geometry-metrics] ok merged=${geo.metrics.length} -> ${rawAbs}`
78
+ );
79
+ }
80
+
81
+ main();
@@ -11,11 +11,28 @@ const ROOT = process.cwd();
11
11
  const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
12
12
  const INDEX_FILE_NAME = process.env.FIGMA_CACHE_INDEX_FILE || "index.json";
13
13
  const DEFAULT_CONTRACT_PATH = "figma-cache/adapters/ui-adapter.contract.json";
14
- const DEFAULT_REPORT_PATH = "figma-cache/reports/ui-1to1-report.json";
14
+ const DEFAULT_REPORT_PATH = "figma-cache/reports/runtime/ui-1to1-report.json";
15
15
  const DEFAULT_MIN_SCORE = 85;
16
16
  const DEFAULT_RECIPES_DIR = "figma-cache/adapters/recipes";
17
17
  const FAIL_EXIT_CODE = 2;
18
18
 
19
+ function parseBoolEnv(value, fallback) {
20
+ if (value == null) return fallback;
21
+ const v = String(value).trim().toLowerCase();
22
+ if (["1", "true", "yes", "on"].includes(v)) return true;
23
+ if (["0", "false", "no", "off"].includes(v)) return false;
24
+ return fallback;
25
+ }
26
+
27
+ function filterRemoteFigmaAssetRefs(input) {
28
+ const text = String(input || "");
29
+ // Mask Figma MCP asset URLs (often require auth; non-deterministic in runtime).
30
+ // This keeps audits stable when teams choose to not ship remote figma assets.
31
+ return text
32
+ .replace(/https:\/\/www\.figma\.com\/api\/mcp\/asset\/[a-z0-9-]+/gi, "__FIGMA_MCP_ASSET__")
33
+ .replace(/\bimg[A-Za-z0-9_]*\s*=\s*['"]https:\/\/www\.figma\.com\/api\/mcp\/asset\/[a-z0-9-]+['"]/gi, "img__=__FIGMA_MCP_ASSET__");
34
+ }
35
+
19
36
  function normalizeSlash(input) {
20
37
  return String(input || "").replace(/\\/g, "/");
21
38
  }
@@ -51,6 +68,7 @@ function parseArgs(argv) {
51
68
  reportPath: DEFAULT_REPORT_PATH,
52
69
  minScore: DEFAULT_MIN_SCORE,
53
70
  recipesDir: DEFAULT_RECIPES_DIR,
71
+ filterRemoteFigmaAssets: parseBoolEnv(process.env.FIGMA_UI_FILTER_REMOTE_FIGMA_ASSETS, true),
54
72
  unknownArgs: [],
55
73
  };
56
74
 
@@ -80,6 +98,10 @@ function parseArgs(argv) {
80
98
  options.recipesDir = arg.split("=").slice(1).join("=").trim() || DEFAULT_RECIPES_DIR;
81
99
  return;
82
100
  }
101
+ if (arg === "--no-filter-remote-figma-assets") {
102
+ options.filterRemoteFigmaAssets = false;
103
+ return;
104
+ }
83
105
  options.unknownArgs.push(arg);
84
106
  });
85
107
 
@@ -170,7 +192,7 @@ function detectMatchedRecipes(recipes, contextText, statesInCache) {
170
192
  }
171
193
 
172
194
  function scoreItem(params) {
173
- const { cacheKey, item, contract, targetCode, recipes } = params;
195
+ const { cacheKey, item, contract, targetCode, recipes, options } = params;
174
196
  const blocking = [];
175
197
  const warnings = [];
176
198
  const diffs = [];
@@ -261,9 +283,13 @@ function scoreItem(params) {
261
283
  }
262
284
 
263
285
  const hasTodo = normalizedFacts.hasPlaceholder;
286
+ const effectiveTargetCode =
287
+ options && options.filterRemoteFigmaAssets
288
+ ? filterRemoteFigmaAssetRefs(targetCode)
289
+ : String(targetCode || "");
264
290
  const matchedRecipes = detectMatchedRecipes(
265
291
  recipes,
266
- `${specText}\n${stateMapText}\n${JSON.stringify(rawJson || {})}\n${targetCode}`,
292
+ `${specText}\n${stateMapText}\n${JSON.stringify(rawJson || {})}\n${effectiveTargetCode}`,
267
293
  statesInCache
268
294
  );
269
295
  if (!matchedRecipes.length) {
@@ -283,11 +309,11 @@ function scoreItem(params) {
283
309
  : textFacts.length;
284
310
  const tokenCodeHits = hasTargetCode
285
311
  ? tokenFacts.filter((fact) =>
286
- targetCode.toUpperCase().includes(String(normalizeHexColor(fact.value || "")).toUpperCase())
312
+ effectiveTargetCode.toUpperCase().includes(String(normalizeHexColor(fact.value || "")).toUpperCase())
287
313
  ).length
288
314
  : tokenFacts.length;
289
315
  const stateCodeHits = hasTargetCode
290
- ? statesInCache.filter((state) => targetCode.toLowerCase().includes(state)).length
316
+ ? statesInCache.filter((state) => effectiveTargetCode.toLowerCase().includes(state)).length
291
317
  : statesInCache.length;
292
318
 
293
319
  const layoutScore = entryReady ? 100 : 20;
@@ -361,6 +387,7 @@ function run() {
361
387
  contract,
362
388
  targetCode,
363
389
  recipes,
390
+ options,
364
391
  })
365
392
  );
366
393
 
@@ -409,6 +436,7 @@ function run() {
409
436
  contractPath: normalizeSlash(contractPath),
410
437
  reportPath: normalizeSlash(reportPath),
411
438
  recipesDir: normalizeSlash(recipesDir),
439
+ filterRemoteFigmaAssets: options.filterRemoteFigmaAssets,
412
440
  },
413
441
  blocking,
414
442
  warnings,
@@ -105,13 +105,13 @@ function buildReportPaths(options) {
105
105
  const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
106
106
  return {
107
107
  preflight: resolveMaybeAbsolutePath(
108
- options.preflightReport || path.join(cacheDir, "reports", "ui-preflight-report.json")
108
+ options.preflightReport || path.join(cacheDir, "reports", "runtime", "ui-preflight-report.json")
109
109
  ),
110
110
  audit: resolveMaybeAbsolutePath(
111
- options.auditReport || path.join(cacheDir, "reports", "ui-1to1-report.json")
111
+ options.auditReport || path.join(cacheDir, "reports", "runtime", "ui-1to1-report.json")
112
112
  ),
113
113
  summary: resolveMaybeAbsolutePath(
114
- options.summaryReport || path.join(cacheDir, "reports", "ui-quality-summary.json")
114
+ options.summaryReport || path.join(cacheDir, "reports", "runtime", "ui-quality-summary.json")
115
115
  ),
116
116
  };
117
117
  }
@@ -10,7 +10,7 @@ const ROOT = process.cwd();
10
10
  const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
11
11
  const INDEX_FILE_NAME = process.env.FIGMA_CACHE_INDEX_FILE || "index.json";
12
12
  const DEFAULT_CONTRACT_PATH = "figma-cache/adapters/ui-adapter.contract.json";
13
- const DEFAULT_REPORT_PATH = "figma-cache/reports/ui-preflight-report.json";
13
+ const DEFAULT_REPORT_PATH = "figma-cache/reports/runtime/ui-preflight-report.json";
14
14
  const BLOCKING_EXIT_CODE = 2;
15
15
 
16
16
  function normalizeSlash(input) {
@@ -8,7 +8,7 @@ const { getUiProfileConfig } = require("./ui-profile");
8
8
 
9
9
  const ROOT = process.cwd();
10
10
  const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
11
- const DEFAULT_OUTPUT_PATH = "figma-cache/reports/ui-quality-summary.json";
11
+ const DEFAULT_OUTPUT_PATH = "figma-cache/reports/runtime/ui-quality-summary.json";
12
12
 
13
13
  function resolveMaybeAbsolutePath(input) {
14
14
  if (!input) {
@@ -62,10 +62,10 @@ function run() {
62
62
  const options = parseArgs(process.argv.slice(2));
63
63
  const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
64
64
  const preflightPath = resolveMaybeAbsolutePath(
65
- options.preflightReport || path.join(cacheDir, "reports", "ui-preflight-report.json")
65
+ options.preflightReport || path.join(cacheDir, "reports", "runtime", "ui-preflight-report.json")
66
66
  );
67
67
  const auditPath = resolveMaybeAbsolutePath(
68
- options.auditReport || path.join(cacheDir, "reports", "ui-1to1-report.json")
68
+ options.auditReport || path.join(cacheDir, "reports", "runtime", "ui-1to1-report.json")
69
69
  );
70
70
  const outputPath = resolveMaybeAbsolutePath(options.output);
71
71
  const profileConfig = getUiProfileConfig();