figma-cache-toolchain 2.0.5 → 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.
- package/figma-cache/docs/README.md +13 -23
- package/figma-cache/docs/raw-json-extensions.schema.json +44 -0
- package/figma-cache/figma-cache.js +111 -0
- package/figma-cache/js/entry-files.js +199 -26
- package/figma-cache/js/flow-cli.js +51 -6
- package/figma-cache/js/raw-derivatives.js +85 -0
- package/figma-cache/js/related-cache-keys.js +56 -0
- package/figma-cache/js/ui-facts-normalizer.js +29 -18
- package/figma-cache/js/validate-cli.js +68 -0
- package/package.json +16 -3
- package/scripts/apply-auto-related-suggestions.cjs +180 -0
- package/scripts/archive-artifacts-from-batch.cjs +160 -0
- package/scripts/auto-link-related-from-batch.cjs +310 -0
- package/scripts/cross-project-e2e.js +19 -3
- package/scripts/forbidden-markup-check.cjs +300 -0
- package/scripts/generate-icon-insets-from-batch.cjs +194 -0
- package/scripts/generate-icon-insets.cjs +112 -0
- package/scripts/import-mcp-raw-evidence.cjs +141 -0
- package/scripts/merge-figma-geometry-metrics.cjs +81 -0
- package/scripts/ui-1to1-audit.js +32 -4
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate iconInsets.<cacheKey>.generated.ts for each batch item.
|
|
6
|
+
* Output directory defaults to the target component directory (dirname(target)).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/generate-icon-insets-from-batch.cjs --batch=./figma-e2e-batch.json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const { execSync } = require("child_process");
|
|
15
|
+
|
|
16
|
+
const ROOT = process.cwd();
|
|
17
|
+
const DEFAULT_INDEX_ABS = path.join(ROOT, "figma-cache", "index.json");
|
|
18
|
+
|
|
19
|
+
function normalizeNodeId(input) {
|
|
20
|
+
const value = String(input || "").trim();
|
|
21
|
+
if (!value) return "";
|
|
22
|
+
return value.includes(":") ? value : value.replace(/-/g, ":");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cacheKeyFromItem(item) {
|
|
26
|
+
const fileKey = String(item && item.fileKey ? item.fileKey : "").trim();
|
|
27
|
+
const nodeId = String(item && item.nodeId ? item.nodeId : "").trim();
|
|
28
|
+
if (!fileKey || !nodeId) return "";
|
|
29
|
+
return `${fileKey}#${normalizeNodeId(nodeId)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeCacheKey(input) {
|
|
33
|
+
const value = String(input || "").trim();
|
|
34
|
+
if (!value) return "";
|
|
35
|
+
const parts = value.split("#");
|
|
36
|
+
if (parts.length !== 2) return value;
|
|
37
|
+
return `${parts[0]}#${normalizeNodeId(parts[1])}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toRelatedCacheKeys(item) {
|
|
41
|
+
const raw = item && item.relatedCacheKeys;
|
|
42
|
+
if (!raw) return [];
|
|
43
|
+
if (Array.isArray(raw)) return raw.map(normalizeCacheKey).filter(Boolean);
|
|
44
|
+
if (typeof raw === "string") {
|
|
45
|
+
return raw
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((s) => normalizeCacheKey(s))
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractCacheKeyFromFigmaUrl(url) {
|
|
54
|
+
const input = String(url || "").trim();
|
|
55
|
+
if (!input) return "";
|
|
56
|
+
// Matches:
|
|
57
|
+
// https://www.figma.com/design/<fileKey>/... ?node-id=9277-28552
|
|
58
|
+
// https://www.figma.com/file/<fileKey>/... ?node-id=9277%3A28552
|
|
59
|
+
const fileKeyMatch = input.match(/figma\.com\/(?:design|file)\/([^/]+)/i);
|
|
60
|
+
const nodeIdMatch = input.match(/[?&]node-id=([^&]+)/i);
|
|
61
|
+
if (!fileKeyMatch || !nodeIdMatch) return "";
|
|
62
|
+
const fileKey = String(fileKeyMatch[1] || "").trim();
|
|
63
|
+
const decodedNode = decodeURIComponent(String(nodeIdMatch[1] || "").trim());
|
|
64
|
+
const nodeId = normalizeNodeId(decodedNode);
|
|
65
|
+
if (!fileKey || !nodeId) return "";
|
|
66
|
+
return `${fileKey}#${nodeId}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toRelatedUrls(item) {
|
|
70
|
+
const raw = item && item.relatedUrls;
|
|
71
|
+
if (!raw) return [];
|
|
72
|
+
if (Array.isArray(raw)) return raw.map((u) => String(u || "").trim()).filter(Boolean);
|
|
73
|
+
if (typeof raw === "string") {
|
|
74
|
+
return raw
|
|
75
|
+
.split(",")
|
|
76
|
+
.map((s) => String(s || "").trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function safeReadJson(absPath) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(fs.readFileSync(absPath, "utf8"));
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function relatedFromFlowIndex(cacheKey) {
|
|
91
|
+
const enabled = process.env.FIGMA_UI_RELATED_FROM_FLOW !== "0";
|
|
92
|
+
if (!enabled) return [];
|
|
93
|
+
const index = safeReadJson(DEFAULT_INDEX_ABS);
|
|
94
|
+
if (!index || typeof index !== "object" || !index.flows) return [];
|
|
95
|
+
const flows = index.flows;
|
|
96
|
+
const related = new Set();
|
|
97
|
+
Object.keys(flows).forEach((flowId) => {
|
|
98
|
+
const flow = flows[flowId];
|
|
99
|
+
const nodes = flow && Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
100
|
+
if (!nodes.includes(cacheKey)) return;
|
|
101
|
+
nodes.forEach((k) => {
|
|
102
|
+
if (k && k !== cacheKey) related.add(normalizeCacheKey(k));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
return Array.from(related).filter(Boolean);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveTargetAbs(rawTarget) {
|
|
109
|
+
const trimmed = String(rawTarget || "").trim();
|
|
110
|
+
if (!trimmed) return "";
|
|
111
|
+
return path.isAbsolute(trimmed) ? path.normalize(trimmed) : path.join(ROOT, trimmed);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseArgs(argv) {
|
|
115
|
+
const out = {
|
|
116
|
+
batch: path.join(ROOT, "figma-e2e-batch.json"),
|
|
117
|
+
maxBox: 24,
|
|
118
|
+
toolchainGenerateScript: path.join(__dirname, "generate-icon-insets.cjs"),
|
|
119
|
+
};
|
|
120
|
+
argv.slice(2).forEach((arg) => {
|
|
121
|
+
if (arg.startsWith("--batch=")) out.batch = arg.split("=").slice(1).join("=").trim();
|
|
122
|
+
if (arg.startsWith("--max-box=")) out.maxBox = Number(arg.split("=").slice(1).join("=").trim());
|
|
123
|
+
});
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function main() {
|
|
128
|
+
const args = parseArgs(process.argv);
|
|
129
|
+
const batchAbs = path.isAbsolute(args.batch) ? args.batch : path.join(ROOT, args.batch);
|
|
130
|
+
if (!fs.existsSync(batchAbs)) {
|
|
131
|
+
console.error(`[generate-icon-insets-from-batch] batch not found: ${batchAbs}`);
|
|
132
|
+
process.exit(2);
|
|
133
|
+
}
|
|
134
|
+
const payload = JSON.parse(fs.readFileSync(batchAbs, "utf8"));
|
|
135
|
+
if (!Array.isArray(payload) || payload.length === 0) {
|
|
136
|
+
console.error("[generate-icon-insets-from-batch] batch must be a non-empty array");
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const genAbs = args.toolchainGenerateScript;
|
|
141
|
+
if (!fs.existsSync(genAbs)) {
|
|
142
|
+
console.error(`[generate-icon-insets-from-batch] missing generator: ${genAbs}`);
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const outputs = [];
|
|
147
|
+
payload.forEach((item, idx) => {
|
|
148
|
+
const cacheKey = String(item && (item.cacheKey || cacheKeyFromItem(item)) || "").trim();
|
|
149
|
+
const targetAbs = resolveTargetAbs(item && item.target);
|
|
150
|
+
const relatedCacheKeysExplicit = toRelatedCacheKeys(item);
|
|
151
|
+
const relatedCacheKeysFromUrls = toRelatedUrls(item)
|
|
152
|
+
.map(extractCacheKeyFromFigmaUrl)
|
|
153
|
+
.map(normalizeCacheKey)
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
const relatedCacheKeysFromFlow = relatedFromFlowIndex(cacheKey);
|
|
156
|
+
const relatedCacheKeys = Array.from(
|
|
157
|
+
new Set([...relatedCacheKeysExplicit, ...relatedCacheKeysFromUrls, ...relatedCacheKeysFromFlow])
|
|
158
|
+
).filter(Boolean);
|
|
159
|
+
if (!cacheKey) {
|
|
160
|
+
console.error(`[generate-icon-insets-from-batch] case[${idx}] missing cacheKey or (fileKey+nodeId)`);
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
if (!targetAbs) {
|
|
164
|
+
console.error(`[generate-icon-insets-from-batch] case[${idx}] missing target`);
|
|
165
|
+
process.exit(2);
|
|
166
|
+
}
|
|
167
|
+
const targetDir = path.dirname(targetAbs);
|
|
168
|
+
const rawArgs = [cacheKey, ...relatedCacheKeys]
|
|
169
|
+
.map((ck) => {
|
|
170
|
+
const [fk, nid] = String(ck).split("#");
|
|
171
|
+
const safeNodeDir = String(nid || "").replace(/:/g, "-");
|
|
172
|
+
const rawAbs = path.join(ROOT, "figma-cache", "files", fk, "nodes", safeNodeDir, "raw.json");
|
|
173
|
+
if (!fs.existsSync(rawAbs)) {
|
|
174
|
+
console.error(`[generate-icon-insets-from-batch] raw.json not found for ${ck}: ${rawAbs}`);
|
|
175
|
+
process.exit(2);
|
|
176
|
+
}
|
|
177
|
+
return `--raw="${rawAbs}"`;
|
|
178
|
+
})
|
|
179
|
+
.join(" ");
|
|
180
|
+
|
|
181
|
+
execSync(`node "${genAbs}" ${rawArgs} --out-dir="${targetDir}" --cacheKey="${cacheKey}" --max-box=${args.maxBox}`, {
|
|
182
|
+
cwd: ROOT,
|
|
183
|
+
stdio: "pipe",
|
|
184
|
+
});
|
|
185
|
+
outputs.push({ cacheKey, outDir: targetDir });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
console.log(
|
|
189
|
+
`[generate-icon-insets-from-batch] ok (${outputs.length} cases)`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main();
|
|
194
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Toolchain-provided icon inset exporter.
|
|
6
|
+
* Reads raw.json.iconMetrics and emits a TS mapping file for machine consumption.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const out = {
|
|
14
|
+
raw: [],
|
|
15
|
+
out: "",
|
|
16
|
+
maxBox: 24,
|
|
17
|
+
cacheKey: "",
|
|
18
|
+
outDir: "",
|
|
19
|
+
};
|
|
20
|
+
argv.slice(2).forEach((arg) => {
|
|
21
|
+
if (arg.startsWith("--raw=")) out.raw.push(arg.split("=").slice(1).join("=").trim());
|
|
22
|
+
if (arg.startsWith("--out=")) out.out = arg.split("=").slice(1).join("=").trim();
|
|
23
|
+
if (arg.startsWith("--out-dir=")) out.outDir = arg.split("=").slice(1).join("=").trim();
|
|
24
|
+
if (arg.startsWith("--cacheKey=")) out.cacheKey = arg.split("=").slice(1).join("=").trim();
|
|
25
|
+
if (arg.startsWith("--max-box=")) out.maxBox = Number(arg.split("=").slice(1).join("=").trim());
|
|
26
|
+
});
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(absPath) {
|
|
31
|
+
return JSON.parse(fs.readFileSync(absPath, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatNumber(n) {
|
|
35
|
+
const num = Number(n);
|
|
36
|
+
if (!Number.isFinite(num)) return "0";
|
|
37
|
+
return String(Number(num.toFixed(4))).replace(/\.0+$/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildTs(mapping) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push(`export type InsetsPx = { top: number; right: number; bottom: number; left: number };`);
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push("/**");
|
|
45
|
+
lines.push(" * AUTO-GENERATED.");
|
|
46
|
+
lines.push(" * Source: raw.json.iconMetrics (derived from get_design_context inset percentages)");
|
|
47
|
+
lines.push(" */");
|
|
48
|
+
lines.push("export const ICON_INSETS_PX: Record<string, InsetsPx> = {");
|
|
49
|
+
Object.keys(mapping)
|
|
50
|
+
.sort()
|
|
51
|
+
.forEach((key) => {
|
|
52
|
+
const v = mapping[key];
|
|
53
|
+
lines.push(
|
|
54
|
+
` "${key}": { top: ${formatNumber(v.top)}, right: ${formatNumber(v.right)}, bottom: ${formatNumber(
|
|
55
|
+
v.bottom
|
|
56
|
+
)}, left: ${formatNumber(v.left)} },`
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
lines.push("};");
|
|
60
|
+
lines.push("");
|
|
61
|
+
return `${lines.join("\n")}\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function main() {
|
|
65
|
+
const args = parseArgs(process.argv);
|
|
66
|
+
if (!args.raw.length || (!args.out && !(args.outDir && args.cacheKey))) {
|
|
67
|
+
console.error(
|
|
68
|
+
"Usage: node scripts/generate-icon-insets.cjs --raw=<raw.json> [--raw=<raw2.json> ...] (--out=<out.ts> | --out-dir=<dir> --cacheKey=<cacheKey>) [--max-box=24]"
|
|
69
|
+
);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
const computedOut = args.out
|
|
73
|
+
? args.out
|
|
74
|
+
: path.join(
|
|
75
|
+
args.outDir,
|
|
76
|
+
`iconInsets.${String(args.cacheKey || "")
|
|
77
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
78
|
+
.slice(0, 120)}.generated.ts`
|
|
79
|
+
);
|
|
80
|
+
const outAbs = path.isAbsolute(computedOut) ? computedOut : path.join(process.cwd(), computedOut);
|
|
81
|
+
|
|
82
|
+
const mapping = {};
|
|
83
|
+
args.raw.forEach((rawPath) => {
|
|
84
|
+
const rawAbs = path.isAbsolute(rawPath) ? rawPath : path.join(process.cwd(), rawPath);
|
|
85
|
+
const data = readJson(rawAbs);
|
|
86
|
+
const list = Array.isArray(data && data.iconMetrics) ? data.iconMetrics : [];
|
|
87
|
+
list.forEach((item) => {
|
|
88
|
+
if (!item || typeof item !== "object") return;
|
|
89
|
+
const nodeId = String(item.nodeId || "").trim();
|
|
90
|
+
const boxPx = Number(item.boxPx);
|
|
91
|
+
const insetPx = item.insetPx;
|
|
92
|
+
if (!nodeId) return;
|
|
93
|
+
if (!Number.isFinite(boxPx) || boxPx <= 0 || boxPx > args.maxBox) return;
|
|
94
|
+
if (!insetPx || typeof insetPx !== "object") return;
|
|
95
|
+
// First write wins to keep stable precedence: primary cacheKey first, related next.
|
|
96
|
+
if (mapping[nodeId]) return;
|
|
97
|
+
mapping[nodeId] = {
|
|
98
|
+
top: Number(insetPx.top || 0),
|
|
99
|
+
right: Number(insetPx.right || 0),
|
|
100
|
+
bottom: Number(insetPx.bottom || 0),
|
|
101
|
+
left: Number(insetPx.left || 0),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
fs.mkdirSync(path.dirname(outAbs), { recursive: true });
|
|
107
|
+
fs.writeFileSync(outAbs, buildTs(mapping), "utf8");
|
|
108
|
+
console.log(`[generate-icon-insets] wrote ${Object.keys(mapping).length} entries -> ${outAbs}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
main();
|
|
112
|
+
|
|
@@ -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();
|
package/scripts/ui-1to1-audit.js
CHANGED
|
@@ -16,6 +16,23 @@ 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${
|
|
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
|
-
|
|
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) =>
|
|
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,
|