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,300 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Toolchain-provided forbidden markup check.
6
+ * Projects can call this script directly from node_modules to hard-fail on disallowed UI patterns.
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+
12
+ const ROOT = process.cwd();
13
+
14
+ const DEFAULT_CONSTRAINTS_PATH = path.join(ROOT, "ui-hard-constraints.json");
15
+ const DEFAULT_POLICY_PATH = path.join(ROOT, "ui-policy.json");
16
+ const DEFAULT_PLATFORM = "web-vue";
17
+
18
+ function parseArgs(argv) {
19
+ const out = {
20
+ batch: path.join(ROOT, "figma-e2e-batch.json"),
21
+ files: [],
22
+ constraints: DEFAULT_CONSTRAINTS_PATH,
23
+ policy: DEFAULT_POLICY_PATH,
24
+ platform: DEFAULT_PLATFORM,
25
+ adapter: "",
26
+ };
27
+ argv.slice(2).forEach((arg) => {
28
+ if (arg.startsWith("--batch=")) {
29
+ out.batch = arg.split("=").slice(1).join("=").trim();
30
+ return;
31
+ }
32
+ if (arg.startsWith("--file=")) {
33
+ out.files.push(arg.split("=").slice(1).join("=").trim());
34
+ return;
35
+ }
36
+ if (arg.startsWith("--constraints=")) {
37
+ out.constraints = arg.split("=").slice(1).join("=").trim();
38
+ return;
39
+ }
40
+ if (arg.startsWith("--policy=")) {
41
+ out.policy = arg.split("=").slice(1).join("=").trim();
42
+ return;
43
+ }
44
+ if (arg.startsWith("--platform=")) {
45
+ out.platform = arg.split("=").slice(1).join("=").trim();
46
+ return;
47
+ }
48
+ if (arg.startsWith("--adapter=")) {
49
+ out.adapter = arg.split("=").slice(1).join("=").trim();
50
+ return;
51
+ }
52
+ });
53
+ return out;
54
+ }
55
+
56
+ function readJsonIfExists(absPath) {
57
+ if (!absPath) return null;
58
+ if (!fs.existsSync(absPath)) return null;
59
+ return JSON.parse(fs.readFileSync(absPath, "utf8"));
60
+ }
61
+
62
+ function resolveAdapterPath(platform, adapterArg) {
63
+ if (adapterArg) return adapterArg;
64
+ const file = `ui-adapter.${platform}.json`;
65
+ return path.join(ROOT, file);
66
+ }
67
+
68
+ function normalizePrimitiveDetectors(raw) {
69
+ if (!raw)
70
+ return { forbiddenTags: [], forbiddenAttrPrefixes: [], forbiddenAttrNames: [], forbiddenPatterns: [] };
71
+ const forbiddenTags = Array.isArray(raw.forbiddenTags) ? raw.forbiddenTags.map(String) : [];
72
+ const forbiddenAttrPrefixes = Array.isArray(raw.forbiddenAttrPrefixes)
73
+ ? raw.forbiddenAttrPrefixes.map(String)
74
+ : [];
75
+ const forbiddenAttrNames = Array.isArray(raw.forbiddenAttrNames) ? raw.forbiddenAttrNames.map(String) : [];
76
+ const forbiddenPatterns = Array.isArray(raw.forbiddenPatterns) ? raw.forbiddenPatterns : [];
77
+ return { forbiddenTags, forbiddenAttrPrefixes, forbiddenAttrNames, forbiddenPatterns };
78
+ }
79
+
80
+ function compileConstraintsFromPolicy(policyRaw, adapterRaw) {
81
+ const policy = policyRaw || {};
82
+ const forbiddenPrimitives = Array.isArray(policy.forbiddenPrimitives)
83
+ ? policy.forbiddenPrimitives.map(String)
84
+ : [];
85
+ const primitives = (adapterRaw && adapterRaw.primitives) || {};
86
+
87
+ const acc = {
88
+ forbiddenTags: [],
89
+ forbiddenAttrPrefixes: [],
90
+ forbiddenAttrNames: [],
91
+ forbiddenPatterns: [],
92
+ };
93
+
94
+ forbiddenPrimitives.forEach((primitive) => {
95
+ const detectors = normalizePrimitiveDetectors(primitives[primitive]);
96
+ acc.forbiddenTags.push(...detectors.forbiddenTags);
97
+ acc.forbiddenAttrPrefixes.push(...detectors.forbiddenAttrPrefixes);
98
+ acc.forbiddenAttrNames.push(...detectors.forbiddenAttrNames);
99
+ acc.forbiddenPatterns.push(...detectors.forbiddenPatterns);
100
+ });
101
+
102
+ acc.forbiddenTags = Array.from(new Set(acc.forbiddenTags));
103
+ acc.forbiddenAttrPrefixes = Array.from(new Set(acc.forbiddenAttrPrefixes));
104
+ acc.forbiddenAttrNames = Array.from(new Set(acc.forbiddenAttrNames));
105
+ acc.forbiddenPatterns = acc.forbiddenPatterns.filter(Boolean);
106
+
107
+ return acc;
108
+ }
109
+
110
+ function normalizeConstraints(raw) {
111
+ const g = (raw && raw.global) || raw || {};
112
+ const forbiddenTags = Array.isArray(g.forbiddenTags) ? g.forbiddenTags.map(String) : [];
113
+ const forbiddenAttrPrefixes = Array.isArray(g.forbiddenAttrPrefixes)
114
+ ? g.forbiddenAttrPrefixes.map(String)
115
+ : [];
116
+ const forbiddenAttrNames = Array.isArray(g.forbiddenAttrNames) ? g.forbiddenAttrNames.map(String) : [];
117
+ const forbiddenPatterns = Array.isArray(g.forbiddenPatterns) ? g.forbiddenPatterns : [];
118
+
119
+ return {
120
+ forbiddenTags,
121
+ forbiddenAttrPrefixes,
122
+ forbiddenAttrNames,
123
+ forbiddenPatterns: forbiddenPatterns
124
+ .map((x) => ({
125
+ id: String(x && x.id ? x.id : "pattern"),
126
+ re: new RegExp(String(x && x.pattern ? x.pattern : ""), "i"),
127
+ }))
128
+ .filter((x) => x.re && String(x.re) !== String(/(?:)/i)),
129
+ };
130
+ }
131
+
132
+ function mergeConstraints(base, override) {
133
+ if (!override) return base;
134
+ const next = { ...base };
135
+ if (Array.isArray(override.forbiddenTags)) next.forbiddenTags = override.forbiddenTags.map(String);
136
+ if (Array.isArray(override.forbiddenAttrPrefixes))
137
+ next.forbiddenAttrPrefixes = override.forbiddenAttrPrefixes.map(String);
138
+ if (Array.isArray(override.forbiddenAttrNames)) next.forbiddenAttrNames = override.forbiddenAttrNames.map(String);
139
+ if (Array.isArray(override.forbiddenPatterns)) {
140
+ next.forbiddenPatterns = override.forbiddenPatterns
141
+ .map((x) => ({
142
+ id: String(x && x.id ? x.id : "pattern"),
143
+ re: new RegExp(String(x && x.pattern ? x.pattern : ""), "i"),
144
+ }))
145
+ .filter((x) => x.re && String(x.re) !== String(/(?:)/i));
146
+ }
147
+ return next;
148
+ }
149
+
150
+ function readBatchTargets(batchPath) {
151
+ const abs = path.isAbsolute(batchPath) ? batchPath : path.join(ROOT, batchPath);
152
+ if (!fs.existsSync(abs)) throw new Error(`batch file missing: ${abs}`);
153
+ const payload = JSON.parse(fs.readFileSync(abs, "utf8"));
154
+ if (!Array.isArray(payload) || payload.length === 0) {
155
+ throw new Error("batch file must contain at least one item");
156
+ }
157
+ return payload.map((item, idx) => {
158
+ const target = String(item && item.target ? item.target : "").trim();
159
+ if (!target) throw new Error(`case[${idx}] missing target`);
160
+ const absTarget = path.isAbsolute(target) ? path.normalize(target) : path.join(ROOT, target);
161
+ const constraintsOverride = item && item.constraints ? item.constraints : null;
162
+ const policyOverride = item && item.policy ? item.policy : null;
163
+ return { absTarget, constraintsOverride, policyOverride };
164
+ });
165
+ }
166
+
167
+ function applyPolicyOverride(basePolicy, policyOverride) {
168
+ if (!policyOverride) return basePolicy;
169
+ const next = { ...(basePolicy || {}) };
170
+ const baseForbidden = Array.isArray(next.forbiddenPrimitives) ? next.forbiddenPrimitives.map(String) : [];
171
+ const addForbidden = Array.isArray(policyOverride.forbiddenPrimitives)
172
+ ? policyOverride.forbiddenPrimitives.map(String)
173
+ : [];
174
+ const allow = Array.isArray(policyOverride.allowPrimitives) ? policyOverride.allowPrimitives.map(String) : [];
175
+
176
+ const merged = Array.from(new Set([...baseForbidden, ...addForbidden]));
177
+ next.forbiddenPrimitives = merged.filter((x) => !allow.includes(x));
178
+ return next;
179
+ }
180
+
181
+ function scanFile(absPath, constraints) {
182
+ const content = fs.readFileSync(absPath, "utf8");
183
+ const violations = [];
184
+
185
+ constraints.forbiddenTags.forEach((tag) => {
186
+ const re = new RegExp(`<\\s*${tag}(\\s|>|/)`, "gi");
187
+ if (re.test(content)) violations.push(`forbidden tag: <${tag}>`);
188
+ });
189
+
190
+ constraints.forbiddenAttrPrefixes.forEach((prefix) => {
191
+ const re = new RegExp(`\\s${prefix}[a-z0-9_-]+\\s*=`, "gi");
192
+ if (re.test(content)) violations.push(`forbidden attr prefix: ${prefix}*`);
193
+ });
194
+
195
+ constraints.forbiddenAttrNames.forEach((name) => {
196
+ const re = new RegExp(`\\s${name}\\s*=`, "gi");
197
+ if (re.test(content)) violations.push(`forbidden attr: ${name}`);
198
+ });
199
+
200
+ constraints.forbiddenPatterns.forEach((item) => {
201
+ if (item.re.test(content)) violations.push(`forbidden pattern: ${item.id}`);
202
+ });
203
+
204
+ // Require cursor-pointer on interactive elements (Vue template heuristic).
205
+ const clickLike =
206
+ /<([a-zA-Z][\w-]*)([^>]*)(@click|@pointerdown|@mousedown|@mouseup)\s*=\s*["'][^"']+["']([^>]*)>/g;
207
+ let match = null;
208
+ while ((match = clickLike.exec(content))) {
209
+ const tag = String(match[1] || "");
210
+ const attrs = `${match[2] || ""}${match[4] || ""}`;
211
+ if (!/cursor-pointer/.test(attrs)) {
212
+ violations.push(`missing cursor-pointer on interactive <${tag}>`);
213
+ }
214
+ }
215
+
216
+ // Avoid icon distortion: forbid icon-like img stretching combo.
217
+ const imgSizeFullIconLike =
218
+ /<img[^>]*class\s*=\s*["'][^"']*\bmax-w-none\b[^"']*\binset-0\b[^"']*\bsize-full\b[^"']*["'][^>]*>/gi;
219
+ if (imgSizeFullIconLike.test(content)) {
220
+ violations.push("forbidden icon img classes: max-w-none + inset-0 + size-full (causes icon stretching)");
221
+ }
222
+
223
+ return violations;
224
+ }
225
+
226
+ function main() {
227
+ const args = parseArgs(process.argv);
228
+
229
+ const policyPath = path.isAbsolute(args.policy) ? args.policy : path.join(ROOT, args.policy);
230
+ const adapterPathRaw = resolveAdapterPath(args.platform, args.adapter);
231
+ const adapterPath = path.isAbsolute(adapterPathRaw) ? adapterPathRaw : path.join(ROOT, adapterPathRaw);
232
+
233
+ const policyRaw = readJsonIfExists(policyPath);
234
+ const adapterRaw = readJsonIfExists(adapterPath);
235
+ const canUsePolicy = Boolean(policyRaw && adapterRaw);
236
+
237
+ const constraintsPath = path.isAbsolute(args.constraints) ? args.constraints : path.join(ROOT, args.constraints);
238
+ const legacyConstraintsRaw = readJsonIfExists(constraintsPath) || {
239
+ global: {
240
+ forbiddenTags: ["button", "p", "ul", "li"],
241
+ forbiddenAttrPrefixes: ["aria-"],
242
+ forbiddenAttrNames: ["role", "tabindex"],
243
+ forbiddenPatterns: [
244
+ { id: "custom scrollbar wrapper: scrollbar-hint", pattern: "\\bscrollbar-hint\\b" },
245
+ { id: "custom scrollbar wrapper: hide-native-scrollbar", pattern: "\\bhide-native-scrollbar\\b" },
246
+ ],
247
+ },
248
+ };
249
+
250
+ const globalConstraints = canUsePolicy
251
+ ? normalizeConstraints(compileConstraintsFromPolicy(policyRaw, adapterRaw))
252
+ : normalizeConstraints(legacyConstraintsRaw);
253
+
254
+ const explicitFiles = (args.files || [])
255
+ .map((p) => (path.isAbsolute(p) ? p : path.join(ROOT, p)))
256
+ .filter(Boolean);
257
+
258
+ const batchItems = explicitFiles.length
259
+ ? explicitFiles.map((absTarget) => ({ absTarget, constraintsOverride: null, policyOverride: null }))
260
+ : readBatchTargets(args.batch);
261
+
262
+ const missing = batchItems.map((x) => x.absTarget).filter((p) => !fs.existsSync(p));
263
+ if (missing.length) {
264
+ console.error("[forbidden-markup-check] missing target files:");
265
+ missing.forEach((p) => console.error(`- ${p}`));
266
+ process.exit(2);
267
+ }
268
+
269
+ const allViolations = [];
270
+ batchItems.forEach((item) => {
271
+ const compiledForCase = canUsePolicy
272
+ ? normalizeConstraints(compileConstraintsFromPolicy(applyPolicyOverride(policyRaw, item.policyOverride), adapterRaw))
273
+ : globalConstraints;
274
+ const effective = mergeConstraints(compiledForCase, item.constraintsOverride);
275
+ const violations = scanFile(item.absTarget, effective);
276
+ if (violations.length) {
277
+ allViolations.push({ file: item.absTarget, violations });
278
+ }
279
+ });
280
+
281
+ if (allViolations.length) {
282
+ console.error("[forbidden-markup-check] FAILED. Found forbidden markup.");
283
+ console.error("Rules:");
284
+ console.error(`- tags: ${globalConstraints.forbiddenTags.map((t) => `<${t}>`).join(", ")}`);
285
+ console.error(
286
+ `- attrs: ${globalConstraints.forbiddenAttrNames.join(", ")}, ${globalConstraints.forbiddenAttrPrefixes.join("")}*`
287
+ );
288
+ console.error("");
289
+ allViolations.forEach((item) => {
290
+ console.error(item.file);
291
+ item.violations.forEach((v) => console.error(` - ${v}`));
292
+ });
293
+ process.exit(2);
294
+ }
295
+
296
+ console.log("[forbidden-markup-check] ok");
297
+ }
298
+
299
+ main();
300
+
@@ -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
+