mini-figma-code-connect 0.1.0 → 0.1.3

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.
Files changed (61) hide show
  1. package/LICENSE +13 -9
  2. package/README.md +52 -130
  3. package/dist/build.mjs +222 -0
  4. package/dist/figma-mapping.mjs +611 -0
  5. package/dist/generate-registry.mjs +79 -0
  6. package/dist/install-skill.mjs +85 -0
  7. package/dist/main/code.d.ts +1 -0
  8. package/dist/main/code.js +388 -0
  9. package/dist/main/handle.d.ts +55 -0
  10. package/dist/main/handle.js +167 -0
  11. package/dist/main/mapping-table.generated.json +1 -0
  12. package/dist/main/messages.d.ts +71 -0
  13. package/dist/main/messages.js +1 -0
  14. package/dist/main/schema.d.ts +9 -0
  15. package/dist/main/schema.js +39 -0
  16. package/dist/main/validate.d.ts +16 -0
  17. package/dist/main/validate.js +40 -0
  18. package/dist/runtime/define.d.ts +3 -0
  19. package/dist/runtime/define.js +4 -0
  20. package/{src/runtime/index.ts → dist/runtime/index.d.ts} +3 -14
  21. package/dist/runtime/index.js +9 -0
  22. package/dist/runtime/registry.d.ts +5 -0
  23. package/dist/runtime/registry.generated.d.ts +3 -0
  24. package/dist/runtime/registry.generated.js +1 -0
  25. package/{src/runtime/registry.ts → dist/runtime/registry.js} +7 -8
  26. package/dist/runtime/render.d.ts +6 -0
  27. package/dist/runtime/render.js +27 -0
  28. package/dist/runtime/tagged.d.ts +6 -0
  29. package/dist/runtime/tagged.js +45 -0
  30. package/dist/runtime/types.d.ts +107 -0
  31. package/dist/runtime/types.js +7 -0
  32. package/dist/scaffold-manifest.mjs +80 -0
  33. package/dist/scaffold-plugin.mjs +462 -0
  34. package/dist/ui/ui.d.ts +84 -0
  35. package/dist/ui/ui.js +190 -0
  36. package/package.json +23 -18
  37. package/scripts/build-plugin.mjs +0 -132
  38. package/scripts/copy-mapping-table.mjs +0 -19
  39. package/scripts/figma-mapping/ai-generate.mjs +0 -102
  40. package/scripts/figma-mapping/index.mjs +0 -250
  41. package/scripts/figma-mapping/lib.mjs +0 -112
  42. package/scripts/figma-mapping/registry.mjs +0 -12
  43. package/scripts/figma-mapping/scaffold.mjs +0 -153
  44. package/scripts/generate-registry.mjs +0 -94
  45. package/scripts/install-skill.mjs +0 -79
  46. package/scripts/is-cli-entrypoint.mjs +0 -21
  47. package/scripts/scaffold-manifest.mjs +0 -84
  48. package/scripts/scaffold-plugin.mjs +0 -172
  49. package/src/dev/demo.ts +0 -207
  50. package/src/dev/export.ts +0 -28
  51. package/src/main/code.ts +0 -422
  52. package/src/main/handle.ts +0 -207
  53. package/src/main/messages.ts +0 -49
  54. package/src/main/schema.ts +0 -45
  55. package/src/main/validate.ts +0 -49
  56. package/src/runtime/define.ts +0 -6
  57. package/src/runtime/render.ts +0 -26
  58. package/src/runtime/tagged.ts +0 -46
  59. package/src/runtime/types.ts +0 -86
  60. package/src/ui/ui.ts +0 -271
  61. /package/{src → dist}/ui/ui.html +0 -0
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env node
2
+
3
+ // scripts/figma-mapping/index.mjs
4
+ import fs4 from "node:fs";
5
+ import path4 from "node:path";
6
+ import readline from "node:readline/promises";
7
+ import { stdin, stdout } from "node:process";
8
+
9
+ // scripts/figma-mapping/lib.mjs
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ var ROOT = process.cwd();
13
+ var CONFIG_PATH = path.join(ROOT, "figma-mapping.config.json");
14
+ var MAPPINGS_DIR = path.join(ROOT, "figma-mappings");
15
+ function readConfig() {
16
+ if (!fs.existsSync(CONFIG_PATH)) {
17
+ console.warn(
18
+ `[figma-mapping] \u5F53\u524D\u76EE\u5F55\uFF08${ROOT}\uFF09\u6CA1\u6709 figma-mapping.config.json \u2014\u2014 \u626B\u4E0D\u5230\u4EFB\u4F55\u5019\u9009\u7EC4\u4EF6\u76EE\u5F55\u3002
19
+ \u6284\u5F15\u64CE\u5305\u91CC\u7684 figma-mapping.config.example.json\uFF0C\u5728\u8FD9\u4E2A\u9879\u76EE\u6839\u76EE\u5F55\u5EFA\u4E00\u4EFD\uFF0C\u6539\u6210\u81EA\u5DF1\u7684\u7EC4\u4EF6\u76EE\u5F55\u3002`
20
+ );
21
+ return { paths: {}, importPaths: {} };
22
+ }
23
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")).codeConnect;
24
+ }
25
+ function listCandidateFiles() {
26
+ const { paths } = readConfig();
27
+ const dirs = Object.values(paths ?? {});
28
+ const results = [];
29
+ function walk(dir) {
30
+ if (!fs.existsSync(dir)) return;
31
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
33
+ const full = path.join(dir, entry.name);
34
+ if (entry.isDirectory()) {
35
+ walk(full);
36
+ } else if (/\.tsx?$/.test(entry.name) && !/\.figma\.tsx?$/.test(entry.name)) {
37
+ results.push(path.relative(ROOT, full));
38
+ }
39
+ }
40
+ }
41
+ for (const d of dirs) walk(path.resolve(ROOT, d));
42
+ return results;
43
+ }
44
+ function extractComponentInfo(relPath) {
45
+ const source = fs.readFileSync(path.join(ROOT, relPath), "utf8");
46
+ const nameMatches = [
47
+ ...source.matchAll(/export\s+(?:declare\s+)?(?:const|function|class)\s+([A-Z][A-Za-z0-9]*)/g)
48
+ ].map((m) => m[1]);
49
+ const propsBlocks = [
50
+ ...source.matchAll(/(?:interface|type)\s+([A-Za-z0-9]*Props)\b[^{]*\{([\s\S]*?)\n\}/g)
51
+ ].map((m) => ({ typeName: m[1], body: m[2] }));
52
+ const propNames = /* @__PURE__ */ new Set();
53
+ for (const block of propsBlocks) {
54
+ for (const m of block.body.matchAll(/^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\??:/gm)) {
55
+ propNames.add(m[1]);
56
+ }
57
+ }
58
+ return { relPath, names: nameMatches, propsBlocks, propNames: [...propNames], source };
59
+ }
60
+ function normalize(s) {
61
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
62
+ }
63
+ function tokenOverlap(a, b) {
64
+ const na = normalize(a);
65
+ const nb = normalize(b);
66
+ if (!na || !nb) return 0;
67
+ if (na === nb) return 1;
68
+ if (na.includes(nb) || nb.includes(na)) return 0.7;
69
+ return 0;
70
+ }
71
+ function scoreCandidate(schema, info) {
72
+ const fileBase = path.basename(info.relPath).replace(/\.tsx?$/, "");
73
+ const nameCandidates = [...info.names, fileBase];
74
+ const nameScore = Math.max(0, ...nameCandidates.map((n) => tokenOverlap(schema.componentName, n)));
75
+ const schemaProps = schema.properties.map((p) => normalize(p.name)).filter(Boolean);
76
+ const codeProps = info.propNames.map(normalize);
77
+ const overlapCount = schemaProps.filter((p) => codeProps.some((c) => c === p || c.includes(p) || p.includes(c))).length;
78
+ const propScore = schemaProps.length ? overlapCount / schemaProps.length : 0;
79
+ const score = nameScore * 0.6 + propScore * 0.4;
80
+ return { score, nameScore, propScore, overlapCount, fileBase };
81
+ }
82
+
83
+ // scripts/figma-mapping/scaffold.mjs
84
+ function normalize2(s) {
85
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
86
+ }
87
+ function findUnionLiterals(propsBody, propName) {
88
+ const line = propsBody.split("\n").find((l) => new RegExp(`^\\s*${propName}\\??:`).test(l));
89
+ if (!line) return null;
90
+ const literals = [...line.matchAll(/"([^"]+)"|'([^']+)'/g)].map((m) => m[1] ?? m[2]);
91
+ return literals.length ? literals : null;
92
+ }
93
+ function bestPropMatch(schemaPropName, codePropNames) {
94
+ const target = normalize2(schemaPropName);
95
+ if (!target) return null;
96
+ let best = null;
97
+ for (const name of codePropNames) {
98
+ const n = normalize2(name);
99
+ if (n === target || n.includes(target) || target.includes(n)) {
100
+ if (!best || n.length < normalize2(best).length) best = name;
101
+ }
102
+ }
103
+ return best;
104
+ }
105
+ function varName(p, i, seen) {
106
+ const cleaned = p.name.replace(/[^a-zA-Z0-9]/g, "");
107
+ const base = cleaned || `prop${i + 1}`;
108
+ const camel = base[0].toLowerCase() + base.slice(1);
109
+ let name = /^[0-9]/.test(camel) ? `_${camel}` : camel;
110
+ if (seen.has(name)) {
111
+ let n = 2;
112
+ while (seen.has(`${name}${n}`)) n++;
113
+ name = `${name}${n}`;
114
+ }
115
+ seen.add(name);
116
+ return name;
117
+ }
118
+ function idFrom(name) {
119
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "template";
120
+ }
121
+ function propLine(p, i, codePropNames, propsBody, seen, claimedCodeProps) {
122
+ const v = varName(p, i, seen);
123
+ const guess = bestPropMatch(p.name, codePropNames);
124
+ const matched = guess && !claimedCodeProps.has(guess) ? guess : null;
125
+ if (matched) claimedCodeProps.add(matched);
126
+ switch (p.type) {
127
+ case "TEXT":
128
+ return { line: `const ${v} = instance.getString('${p.name}')`, jsxAttr: matched ? `${matched}={${v}}` : null };
129
+ case "BOOLEAN":
130
+ return { line: `const ${v} = instance.getBoolean('${p.name}')`, jsxAttr: matched ? `${matched}={${v}}` : null };
131
+ case "VARIANT": {
132
+ const literals = matched && propsBody ? findUnionLiterals(propsBody, matched) : null;
133
+ const opts = p.variantOptions ?? [];
134
+ const mapping = opts.map((o) => {
135
+ const guess2 = literals?.find((l) => normalize2(l) === normalize2(o)) ?? literals?.[0];
136
+ const value = guess2 ? JSON.stringify(guess2) : `'TODO'`;
137
+ return ` ${JSON.stringify(o)}: ${value},`;
138
+ }).join("\n");
139
+ return {
140
+ line: `const ${v} = instance.getEnum('${p.name}', {
141
+ ${mapping}
142
+ })`,
143
+ jsxAttr: matched ? `${matched}="\${${v}}"` : null
144
+ };
145
+ }
146
+ case "INSTANCE_SWAP":
147
+ return {
148
+ line: `const ${v} = instance.getInstanceSwap('${p.name}')
149
+ let ${v}Code
150
+ if (${v} && ${v}.type === 'INSTANCE') {
151
+ ${v}Code = ${v}.executeTemplate()?.example
152
+ }`,
153
+ jsxAttr: matched ? `${matched}={\${${v}Code}}` : null
154
+ };
155
+ default:
156
+ return { line: `// TODO: \u672A\u77E5\u5C5E\u6027\u7C7B\u578B ${p.type} \u2014 ${p.name}`, jsxAttr: null };
157
+ }
158
+ }
159
+ function buildMatchedScaffold({ schema, componentFile, componentName, importSpecifier, propsBlock }) {
160
+ const propsBody = propsBlock?.body ?? "";
161
+ const codePropNames = propsBlock ? [...propsBody.matchAll(/^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\??:/gm)].map((m) => m[1]) : [];
162
+ const seen = /* @__PURE__ */ new Set();
163
+ const claimedCodeProps = /* @__PURE__ */ new Set();
164
+ const parts = schema.properties.map((p, i) => propLine(p, i, codePropNames, propsBody, seen, claimedCodeProps));
165
+ const lines = parts.map((p) => ` ${p.line}`).join("\n\n");
166
+ const jsxAttrs = parts.map((p) => p.jsxAttr).filter(Boolean).map((a) => `
167
+ ${a}`).join("");
168
+ const unmatchedCount = schema.properties.length - parts.filter((p) => p.jsxAttr).length;
169
+ const id = idFrom(schema.componentName);
170
+ return `// url=https://www.figma.com/design/REPLACE_FILE_KEY/Design-System?node-id=REPLACE
171
+ // source=${componentFile}
172
+ // component=${componentName}
173
+ import { defineTemplate } from '../runtime/define'
174
+ import { code } from '../runtime/tagged'
175
+
176
+ /**
177
+ * \u7531 figma-mapping CLI \u751F\u6210\uFF1AFigma "${schema.componentName}" \u2194 \u4EE3\u7801 ${componentName}\uFF08${componentFile}\uFF09
178
+ * ${unmatchedCount > 0 ? `${unmatchedCount} \u4E2A\u5C5E\u6027\u6CA1\u81EA\u52A8\u914D\u4E0A\u5BF9\u5E94\u7684\u4EE3\u7801 prop\uFF0C\u4EBA\u5DE5\u786E\u8BA4` : "\u6BCF\u4E2A\u5C5E\u6027\u90FD\u81EA\u52A8\u914D\u4E0A\u4E86\u5BF9\u5E94\u7684\u4EE3\u7801 prop\uFF0C\u4ECD\u5EFA\u8BAE\u4EBA\u5DE5\u786E\u8BA4\u4E00\u904D"}
179
+ */
180
+ export default defineTemplate({
181
+ meta: {
182
+ url: 'https://www.figma.com/design/REPLACE_FILE_KEY/Design-System?node-id=REPLACE',
183
+ source: '${componentFile}',
184
+ component: '${componentName}',
185
+ },
186
+ id: '${id}',
187
+ match: { componentName: '${schema.componentName}' },
188
+
189
+ render(instance) {
190
+ ${lines || " // \u8FD9\u4E2A\u7EC4\u4EF6\u6CA1\u6709\u53EF\u8BFB\u7684\u5C5E\u6027"}
191
+
192
+ return {
193
+ example: code\`
194
+ <${componentName}${jsxAttrs}
195
+ />
196
+ \`,
197
+ imports: ['import { ${componentName} } from "${importSpecifier}"'],
198
+ id: '${id}',
199
+ metadata: { nestable: true },
200
+ }
201
+ },
202
+ })
203
+ `;
204
+ }
205
+ function toImportSpecifier(relPath, importPaths) {
206
+ for (const [glob, spec] of Object.entries(importPaths ?? {})) {
207
+ const prefix = glob.replace(/\*$/, "");
208
+ if (relPath.startsWith(prefix)) {
209
+ const rest = relPath.slice(prefix.length).replace(/\.tsx?$/, "");
210
+ return spec.endsWith("*") ? spec.replace(/\*$/, rest) : spec;
211
+ }
212
+ }
213
+ return "./" + relPath.replace(/\.tsx?$/, "");
214
+ }
215
+
216
+ // scripts/figma-mapping/registry.mjs
217
+ import fs3 from "node:fs";
218
+ import path3 from "node:path";
219
+
220
+ // scripts/generate-registry.mjs
221
+ import fs2 from "node:fs";
222
+ import path2 from "node:path";
223
+
224
+ // scripts/is-cli-entrypoint.mjs
225
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
226
+ import { fileURLToPath } from "node:url";
227
+ function isCliEntrypoint(importMetaUrl) {
228
+ if (!process.argv[1]) return false;
229
+ try {
230
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(importMetaUrl));
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ // scripts/generate-registry.mjs
237
+ function parseGlob(glob) {
238
+ const starIdx = glob.indexOf("*");
239
+ if (starIdx === -1) throw new Error(`generate-registry: glob \u91CC\u6CA1\u6709 *\uFF1A"${glob}"`);
240
+ const dir = glob.slice(0, starIdx).replace(/\/$/, "");
241
+ const recursive = glob.includes("**");
242
+ return { dir, recursive };
243
+ }
244
+ function findFigmaFiles(dir, recursive) {
245
+ const results = [];
246
+ function walk(d) {
247
+ if (!fs2.existsSync(d)) return;
248
+ for (const entry of fs2.readdirSync(d, { withFileTypes: true })) {
249
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
250
+ const full = path2.join(d, entry.name);
251
+ if (entry.isDirectory()) {
252
+ if (recursive) walk(full);
253
+ } else if (entry.name.endsWith(".figma.ts")) {
254
+ results.push(full);
255
+ }
256
+ }
257
+ }
258
+ walk(dir);
259
+ return results.sort();
260
+ }
261
+ function identifierFor(filePath, index) {
262
+ const base = path2.basename(filePath).replace(/\.figma\.ts$/, "").replace(/[^a-zA-Z0-9]/g, "");
263
+ const safe = base && /^[a-zA-Z_]/.test(base) ? base : `M${base}`;
264
+ return `${safe || "Mapping"}_${index}`;
265
+ }
266
+ function generateRegistry({ cwd, mappingsGlob, outFile, typesImport = "./types" }) {
267
+ const { dir, recursive } = parseGlob(mappingsGlob);
268
+ const files = findFigmaFiles(path2.resolve(cwd, dir), recursive);
269
+ const outDir = path2.dirname(outFile);
270
+ const imports = files.map((f, i) => {
271
+ const id = identifierFor(f, i);
272
+ let rel = path2.relative(outDir, f).replace(/\.ts$/, "");
273
+ if (!rel.startsWith(".")) rel = `./${rel}`;
274
+ rel = rel.split(path2.sep).join("/");
275
+ return { id, importPath: rel };
276
+ });
277
+ const typeLine = typesImport == null ? "" : `import type { Template } from '${typesImport}'
278
+ `;
279
+ const templatesAnn = typesImport == null ? "" : ": Template[]";
280
+ const body = `// \u81EA\u52A8\u751F\u6210\uFF0C\u4E0D\u8981\u624B\u6539 \u2014\u2014 \u7531 scripts/generate-registry.mjs \u626B .figma.ts \u751F\u6210\uFF0C\u6BCF\u6B21 build \u524D\u91CD\u8DD1
281
+ ` + typeLine + imports.map((i) => `import ${i.id} from '${i.importPath}'`).join("\n") + (imports.length ? "\n\n" : "\n") + `export const templates${templatesAnn} = [${imports.map((i) => i.id).join(", ")}]
282
+ `;
283
+ fs2.mkdirSync(outDir, { recursive: true });
284
+ fs2.writeFileSync(outFile, body);
285
+ return { count: files.length, files };
286
+ }
287
+ if (isCliEntrypoint(import.meta.url)) {
288
+ const [, , mappingsGlob, outFile] = process.argv;
289
+ if (!mappingsGlob || !outFile) {
290
+ console.error("\u7528\u6CD5: node scripts/generate-registry.mjs <mappingsGlob> <outFile>");
291
+ process.exit(1);
292
+ }
293
+ const result = generateRegistry({ cwd: process.cwd(), mappingsGlob, outFile: path2.resolve(outFile) });
294
+ console.log(`[generate-registry] \u5199\u5165 ${result.count} \u6761\u6620\u5C04\u5230 ${outFile}`);
295
+ }
296
+
297
+ // scripts/figma-mapping/registry.mjs
298
+ function refreshRegistry({
299
+ cwd,
300
+ mappingsGlob = "figma-mappings/**/*.figma.ts",
301
+ outFile = path3.join(cwd, "figma-plugin-dist/.generated/registry.generated.ts")
302
+ }) {
303
+ fs3.mkdirSync(path3.dirname(outFile), { recursive: true });
304
+ const result = generateRegistry({ cwd, mappingsGlob, outFile });
305
+ return { count: result.count };
306
+ }
307
+
308
+ // scripts/figma-mapping/ai-generate.mjs
309
+ import { execFileSync } from "node:child_process";
310
+ var EXAMPLE_TEMPLATE = `import { defineTemplate, code } from 'mini-figma-code-connect'
311
+
312
+ export default defineTemplate({
313
+ meta: {
314
+ url: 'https://www.figma.com/design/FILE_KEY/Design-System?node-id=NODE_ID',
315
+ source: '../path/to/components/button.tsx',
316
+ component: 'Button',
317
+ },
318
+ id: 'button',
319
+ match: { componentName: 'Button' },
320
+
321
+ render(instance) {
322
+ const label = instance.getString('Label')
323
+ const disabled = instance.getBoolean('Disabled')
324
+ const size = instance.getEnum('Size', { Large: 'large', Medium: 'medium', Small: 'small' })
325
+
326
+ return {
327
+ example: code\`<Button size="\${size}"\${disabled ? code\` disabled\` : ''}>\${label}</Button>\`,
328
+ imports: ['import { Button } from "@/components/button"'],
329
+ id: 'button',
330
+ }
331
+ },
332
+ })
333
+ `;
334
+ function buildPrompt({ schema, candidates, importPaths }) {
335
+ const candidateBlocks = candidates.map(
336
+ (c) => `### \u5019\u9009\u6587\u4EF6: ${c.relPath}
337
+ \`\`\`
338
+ ${c.source}
339
+ \`\`\`
340
+ `
341
+ ).join("\n");
342
+ return `\u4F60\u662F Figma Code Connect \u6620\u5C04\u751F\u6210\u5668\u3002\u4EFB\u52A1\uFF1A\u5224\u65AD\u4E0B\u9762\u8FD9\u4E2A Figma \u7EC4\u4EF6\u8BE5\u63A5\u5230\u54EA\u4E2A\u5019\u9009\u4EE3\u7801\u7EC4\u4EF6\uFF0C\u5982\u679C\u6709\u5408\u9002\u7684\u5C31\u751F\u6210\u6620\u5C04\u6587\u4EF6\u5185\u5BB9\u3002
343
+
344
+ ## Figma \u7EC4\u4EF6 schema
345
+ \`\`\`json
346
+ ${JSON.stringify(schema, null, 2)}
347
+ \`\`\`
348
+
349
+ ## \u5019\u9009\u4EE3\u7801\u7EC4\u4EF6\uFF08\u6309\u8DEF\u5F84\u6392\u5217\uFF0C\u53EF\u80FD\u4E00\u4E2A\u90FD\u4E0D\u5408\u9002\uFF09
350
+ ${candidateBlocks}
351
+
352
+ ## \u76EE\u6807\u6587\u4EF6\u683C\u5F0F\uFF08\u8FD9\u662F\u4E00\u4EFD\u5DF2\u6709\u7684\u793A\u4F8B\uFF0C\u7167\u8FD9\u4E2A\u683C\u5F0F\u548C accessor API \u5199\uFF0C\u4E0D\u8981\u7528\u522B\u7684\u683C\u5F0F\uFF09
353
+ \`\`\`ts
354
+ ${EXAMPLE_TEMPLATE}
355
+ \`\`\`
356
+
357
+ ## import \u8DEF\u5F84\u6362\u7B97\u89C4\u5219
358
+ \u5019\u9009\u6587\u4EF6\u8DEF\u5F84\u524D\u7F00 \u2192 \u751F\u6210\u6587\u4EF6\u91CC imports \u7528\u7684 specifier\uFF1A
359
+ ${JSON.stringify(importPaths, null, 2)}
360
+
361
+ ## \u8981\u6C42
362
+ 1. \u4ECE\u5019\u9009\u6587\u4EF6\u91CC\u9009\u4E00\u4E2A\u6700\u5408\u9002\u7684\uFF08\u7EC4\u4EF6\u540D\u3001Props \u8BED\u4E49\u90FD\u8981\u5BF9\u5F97\u4E0A\uFF0C\u4E0D\u662F\u5B57\u7B26\u4E32\u50CF\u4E0D\u50CF\uFF09\u3002\u4E00\u4E2A\u90FD\u4E0D\u5408\u9002\u5C31\u9009 NONE\u3002
363
+ 2. Figma \u5C5E\u6027\u540D\u53EF\u80FD\u662F\u4E2D\u6587\u3001\u53EF\u80FD\u8DDF\u4EE3\u7801 prop \u4E0D\u662F\u5B57\u9762\u4E00\u81F4\uFF08\u6BD4\u5982"\u72B6\u6001"\u91CC\u7684 disabled/hover/loading \u9700\u8981\u62C6\u6210 disabled/loading \u4E24\u4E2A prop\uFF0Chover/pressed \u662F CSS \u72B6\u6001\u4E0D\u662F prop\uFF0C\u4E0D\u80FD\u786C\u585E\uFF09\u3002VARIANT \u7684\u5B57\u5178\u8981\u8986\u76D6 schema \u91CC\u7ED9\u7684\u6BCF\u4E00\u4E2A\u9009\u9879\uFF0C\u914D\u4E0D\u4E0A\u771F\u5B9E\u503C\u7684\u5FC5\u987B\u663E\u5F0F\u7559 'TODO' \u4E0D\u8981\u778E\u7F16\u4E00\u4E2A\u80FD\u7F16\u8BD1\u4F46\u8BED\u4E49\u9519\u7684\u503C\u3002
364
+ 3. \u4E0D\u786E\u5B9A\u7684\u5224\u65AD\uFF08\u6BD4\u5982\u6CA1\u6709\u7CBE\u786E\u5BF9\u5E94\u7684 variant \u503C\u8BE5\u731C\u54EA\u4E2A\uFF09\uFF0C\u7167\u6837\u505A\u51FA\u9009\u62E9\uFF0C\u4F46\u5728\u751F\u6210\u6587\u4EF6\u9876\u90E8\u6CE8\u91CA\u91CC\u660E\u786E\u5199\u51FA"\u8FD9\u662F\u731C\u7684\uFF0C\u9700\u8981\u4EBA\u5DE5\u786E\u8BA4"\uFF0C\u522B\u4E0D\u58F0\u4E0D\u54CD\u3002
365
+ 4. \u4E25\u7981\u8C03\u7528\u4EFB\u4F55\u5DE5\u5177/\u547D\u4EE4\uFF0C\u4E25\u7981\u8BFB\u5199\u6587\u4EF6\u7CFB\u7EDF \u2014\u2014 \u4F60\u73B0\u5728\u6CA1\u6709\u8FD9\u4E9B\u6743\u9650\uFF0C\u53EA\u9700\u8981\u57FA\u4E8E\u4E0A\u9762\u7ED9\u7684\u4FE1\u606F\u76F4\u63A5\u751F\u6210\u6587\u672C\u3002
366
+ 5. \u4E25\u7981\u7F16\u9020\u4EE3\u7801\u91CC\u4E0D\u5B58\u5728\u7684 prop \u540D\u3002
367
+
368
+ ## \u8F93\u51FA\u683C\u5F0F\uFF08\u4E25\u683C\u9075\u5B88\uFF0C\u4E0D\u8981\u8F93\u51FA\u5176\u5B83\u4EFB\u4F55\u6587\u5B57\uFF09
369
+ \u7B2C\u4E00\u884C\uFF1A\`MATCH: <\u5019\u9009\u6587\u4EF6\u7684 relPath>\` \u6216 \`MATCH: NONE\`
370
+ \u5982\u679C MATCH \u4E0D\u662F NONE\uFF0C\u7B2C\u4E8C\u884C\u5F00\u59CB\u662F\u4E00\u6761\u5206\u9694\u7EBF \`---\`\uFF0C\u4E4B\u540E\u662F\u5B8C\u6574\u7684 .figma.ts \u6587\u4EF6\u5185\u5BB9\uFF08\u4E0D\u8981\u7528 markdown \u4EE3\u7801\u5757\u5305\u88F9\uFF0C\u76F4\u63A5\u662F\u53EF\u4EE5\u539F\u6837\u5199\u5165\u6587\u4EF6\u7684 TypeScript \u6E90\u7801\uFF09\u3002
371
+ \u5982\u679C MATCH \u662F NONE\uFF0C\u7B2C\u4E8C\u884C\u5F00\u59CB\u7B80\u77ED\u8BF4\u660E\u4E3A\u4EC0\u4E48\u6CA1\u6709\u5408\u9002\u7684\u5019\u9009\uFF08\u4E00\u4E24\u53E5\u8BDD\uFF09\u3002`;
372
+ }
373
+ function aiGenerateMapping({ schema, candidates, importPaths }) {
374
+ const prompt = buildPrompt({ schema, candidates, importPaths });
375
+ const raw = execFileSync(
376
+ "claude",
377
+ [
378
+ "-p",
379
+ prompt,
380
+ "--output-format",
381
+ "text",
382
+ "--disallowedTools",
383
+ "Read,Write,Edit,Bash,Glob,Grep,WebFetch,WebSearch,Task,NotebookEdit"
384
+ ],
385
+ { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 }
386
+ );
387
+ const firstLineEnd = raw.indexOf("\n");
388
+ const firstLine = (firstLineEnd === -1 ? raw : raw.slice(0, firstLineEnd)).trim();
389
+ const rest = firstLineEnd === -1 ? "" : raw.slice(firstLineEnd + 1);
390
+ const m = firstLine.match(/^MATCH:\s*(.+)$/);
391
+ if (!m || m[1].trim() === "NONE") {
392
+ return { matched: false, reason: rest.trim() || firstLine };
393
+ }
394
+ const relPath = m[1].trim();
395
+ const content = rest.replace(/^---\s*\n?/, "");
396
+ return { matched: true, relPath, content };
397
+ }
398
+
399
+ // scripts/figma-mapping/index.mjs
400
+ var rl = readline.createInterface({ input: stdin, output: stdout });
401
+ var ask = (q) => rl.question(q);
402
+ function printHelp() {
403
+ console.log(`\u7528\u6CD5:
404
+ node scripts/figma-mapping/index.mjs <schema.json> [--ai]
405
+
406
+ schema.json \u4ECE\u63D2\u4EF6\u9762\u677F\u62FF\uFF1A
407
+ - \u5355\u4E2A\u7EC4\u4EF6\uFF1A"\u5BFC\u51FA schema.json"\u6309\u94AE\uFF08\u672A\u6620\u5C04\u72B6\u6001\u4E0B\u624D\u6709\uFF09
408
+ - \u4E00\u6574\u6279\uFF1A\u5019\u9009\u5217\u8868\u91CC"\u5BFC\u51FA\u5168\u90E8 schema.json"\u6309\u94AE\uFF0C\u4F1A\u662F\u4E00\u4E2A schema \u6570\u7EC4\uFF0C\u811A\u672C\u6328\u4E2A\u8DD1\u5B8C
409
+
410
+ \u4E0D\u5E26 --ai\uFF1A\u672C\u5730\u6B63\u5219\u6253\u5206\u6392\u5019\u9009\uFF0C\u4F60\u81EA\u5DF1\u6311\u3001\u81EA\u5DF1\u5224\u65AD\u8981\u4E0D\u8981\u63A5\u54EA\u4E2A prop\u3002
411
+ \u5E26 --ai\uFF1A\u8C03\u672C\u673A claude CLI\uFF08\u65E0\u5934\u6A21\u5F0F\uFF0C\u7981\u6389\u6240\u6709\u5DE5\u5177\u6743\u9650\uFF09\u8BFB schema + \u5019\u9009\u6E90\u7801\uFF0C\u76F4\u63A5\u505A\u771F\u5224\u65AD\u3001
412
+ \u751F\u6210\u5B8C\u6574\u6620\u5C04\u5185\u5BB9\uFF0C\u4F60\u53EA\u9700\u8981\u786E\u8BA4\u8981\u4E0D\u8981\u91C7\u7EB3\u2014\u2014\u5224\u65AD\u529B\u6BD4\u6B63\u5219\u5F3A\uFF0C\u4F46\u4E5F\u53EF\u80FD\u5224\u65AD\u9519\uFF0C\u4E00\u6837\u8981\u770B\u4E00\u773C\u3002
413
+
414
+ \u4E24\u79CD\u6A21\u5F0F\u90FD\u4F1A\u628A .figma.ts \u751F\u6210\u5230\u5F53\u524D\u76EE\u5F55\uFF08\u8FD0\u884C\u8FD9\u4E2A\u547D\u4EE4\u7684\u9879\u76EE\u6839\u76EE\u5F55\uFF09\u7684 figma-mappings/\uFF0C\u5E76\u91CD\u65B0\u751F\u6210
415
+ registry.generated.ts\uFF08\u81EA\u52A8\u626B\u5168\u90E8 .figma.ts\uFF0C\u4E0D\u9700\u8981\u624B\u52A8\u52A0 import\uFF09\u3002`);
416
+ }
417
+ function rankFiles(files, schema) {
418
+ return files.map((f) => {
419
+ const info = extractComponentInfo(f);
420
+ const s = scoreCandidate(schema, info);
421
+ return { file: f, info, ...s };
422
+ }).sort((a, b) => b.score - a.score);
423
+ }
424
+ async function main(schemaPath2, useAi) {
425
+ if (!schemaPath2 || !fs4.existsSync(schemaPath2)) {
426
+ console.error(`\u627E\u4E0D\u5230 schema \u6587\u4EF6: ${schemaPath2}
427
+ \u5148\u5728\u63D2\u4EF6\u9762\u677F\u91CC\u5BF9\u7740\u672A\u6620\u5C04\u7684\u5B9E\u4F8B\u70B9"\u5BFC\u51FA schema.json"\u3002`);
428
+ process.exitCode = 1;
429
+ return;
430
+ }
431
+ const parsed = JSON.parse(fs4.readFileSync(schemaPath2, "utf8"));
432
+ const queue = Array.isArray(parsed) ? parsed : [parsed];
433
+ console.log(`
434
+ \u5171 ${queue.length} \u4E2A Figma \u7EC4\u4EF6\u5F85\u5904\u7406\u3002${useAi ? "\uFF08AI \u6A21\u5F0F\uFF09" : ""}
435
+ `);
436
+ await processQueue(queue, useAi);
437
+ rl.close();
438
+ }
439
+ async function processQueue(queue, useAi) {
440
+ if (queue.length === 0) {
441
+ console.log("\n\u5168\u90E8\u5904\u7406\u5B8C\u3002");
442
+ return;
443
+ }
444
+ const [schema, ...rest] = queue;
445
+ console.log(`
446
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [\u8FD8\u5269 ${queue.length} \u4E2A] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
447
+ console.log(`\u76EE\u6807 Figma \u7EC4\u4EF6: ${schema.componentName}`);
448
+ console.log(`\u5C5E\u6027: ${schema.properties.map((p) => `${p.name}(${p.type})`).join(", ") || "\uFF08\u65E0\uFF09"}
449
+ `);
450
+ const allFiles = listCandidateFiles();
451
+ if (useAi) {
452
+ return aiFlow(schema, rankFiles(allFiles, schema), rest);
453
+ }
454
+ await pickLoop(schema, rankFiles(allFiles, schema), allFiles, rest);
455
+ }
456
+ async function aiFlow(schema, ranked, rest) {
457
+ const top = ranked.slice(0, 6);
458
+ if (top.length === 0) {
459
+ console.log("\u6CA1\u6709\u5019\u9009\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u8FD9\u4E2A\u7EC4\u4EF6\u3002");
460
+ return processQueue(rest, true);
461
+ }
462
+ console.log(`\u628A\u8FD9 ${top.length} \u4E2A\u5019\u9009\u8FDE\u540C\u6E90\u7801\u4EA4\u7ED9 claude \u5224\u65AD:`);
463
+ top.forEach((r) => console.log(` - ${r.file}`));
464
+ console.log("\n\u8C03\u7528\u4E2D\uFF08\u53EF\u80FD\u8981\u51E0\u5341\u79D2\uFF09...\n");
465
+ const config = readConfig();
466
+ const candidates = top.map((r) => ({ relPath: r.file, source: r.info.source }));
467
+ let result;
468
+ try {
469
+ result = aiGenerateMapping({ schema, candidates, importPaths: config.importPaths });
470
+ } catch (err) {
471
+ console.log(`\u8C03\u7528 claude CLI \u5931\u8D25\uFF1A${err.stderr || err.stdout || err.message}
472
+ `);
473
+ const fallback = (await ask("\u8981\u4E0D\u8981\u9000\u56DE\u672C\u5730\u6B63\u5219\u6253\u5206\u624B\u52A8\u6311\u4E00\u4E2A\uFF1F(y/N): ")).trim().toLowerCase();
474
+ if (fallback === "y") {
475
+ const allFiles = listCandidateFiles();
476
+ return pickLoop(schema, ranked, allFiles, rest, true);
477
+ }
478
+ return processQueue(rest, true);
479
+ }
480
+ if (!result.matched) {
481
+ console.log(`AI \u5224\u65AD\u6CA1\u6709\u5408\u9002\u7684\u5019\u9009\uFF1A${result.reason}
482
+ `);
483
+ const fallback = (await ask("\u8981\u4E0D\u8981\u9000\u56DE\u672C\u5730\u6B63\u5219\u6253\u5206\u624B\u52A8\u6311\u4E00\u4E2A\uFF1F(y/N): ")).trim().toLowerCase();
484
+ if (fallback === "y") {
485
+ const allFiles = listCandidateFiles();
486
+ return pickLoop(schema, ranked, allFiles, rest, true);
487
+ }
488
+ return processQueue(rest, true);
489
+ }
490
+ console.log(`AI \u9009\u4E86: ${result.relPath}
491
+ `);
492
+ console.log("\u2500".repeat(60));
493
+ console.log(result.content);
494
+ console.log("\u2500".repeat(60));
495
+ const confirm = (await ask('\n\u786E\u8BA4\u91C7\u7EB3\u8FD9\u4EFD\u751F\u6210\u7ED3\u679C\u5417\uFF1F\u8F93\u5165 "good" \u786E\u8BA4\uFF0C\u5176\u4ED6\u4EFB\u610F\u952E\u653E\u5F03: ')).trim();
496
+ if (confirm !== "good") {
497
+ console.log("\u653E\u5F03\uFF0C\u8DF3\u8FC7\u8FD9\u4E2A\u7EC4\u4EF6\u3002\n");
498
+ return processQueue(rest, true);
499
+ }
500
+ return writeMappingFile(schema, result.content, rest, true);
501
+ }
502
+ async function pickLoop(schema, ranked, allFiles, rest, useAi) {
503
+ const top = ranked.slice(0, 8);
504
+ if (top.length === 0) {
505
+ console.log("\u6CA1\u6709\u5019\u9009\u6587\u4EF6\u4E86\uFF0C\u8DF3\u8FC7\u8FD9\u4E2A\u7EC4\u4EF6\u3002");
506
+ return processQueue(rest, useAi);
507
+ }
508
+ console.log("\u5019\u9009\u6392\u540D\uFF08\u5206\u6570\u8D8A\u9AD8\u8D8A\u50CF\uFF09:\n");
509
+ top.forEach((r, i) => {
510
+ const reason = `\u540D\u5B57\u76F8\u4F3C\u5EA6 ${r.nameScore.toFixed(2)} \xB7 \u5C5E\u6027\u91CD\u5408 ${r.overlapCount}/${schema.properties.length}`;
511
+ console.log(` ${i + 1}. ${r.file} [${r.score.toFixed(2)}] (${reason})`);
512
+ });
513
+ const answer = (await ask('\n\u8F93\u5165\u5E8F\u53F7\u9009\u4E00\u4E2A / \u8F93\u5165\u5173\u952E\u5B57\u91CD\u65B0\u7B5B\u9009\u6587\u4EF6 / "m" \u624B\u52A8\u8F93\u5165\u8DEF\u5F84 / "s" \u8DF3\u8FC7\u8FD9\u4E2A\u7EC4\u4EF6 / "q" \u5168\u90E8\u9000\u51FA: ')).trim();
514
+ if (answer === "q") return;
515
+ if (answer === "s") return processQueue(rest, useAi);
516
+ if (answer === "m") {
517
+ const rel = (await ask("\u8F93\u5165\u8DEF\u5F84\uFF08\u76F8\u5BF9\u9879\u76EE\u6839\u76EE\u5F55\uFF0C\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09: ")).trim();
518
+ if (!fs4.existsSync(path4.resolve(ROOT, rel))) {
519
+ console.log("\u6587\u4EF6\u4E0D\u5B58\u5728\uFF0C\u91CD\u6765\u3002\n");
520
+ return pickLoop(schema, ranked, allFiles, rest, useAi);
521
+ }
522
+ return confirmAndGenerate(schema, path4.relative(ROOT, path4.resolve(ROOT, rel)), rest, useAi);
523
+ }
524
+ const idx = Number(answer);
525
+ if (Number.isInteger(idx) && idx >= 1 && idx <= top.length) {
526
+ return confirmAndGenerate(schema, top[idx - 1].file, rest, useAi);
527
+ }
528
+ const filtered = allFiles.filter((f) => f.toLowerCase().includes(answer.toLowerCase()));
529
+ if (filtered.length === 0) {
530
+ console.log("\u6CA1\u5339\u914D\u5230\u6587\u4EF6\uFF0C\u91CD\u6765\u3002\n");
531
+ return pickLoop(schema, ranked, allFiles, rest, useAi);
532
+ }
533
+ console.log("");
534
+ return pickLoop(schema, rankFiles(filtered, schema), allFiles, rest, useAi);
535
+ }
536
+ async function confirmAndGenerate(schema, relPath, rest, useAi) {
537
+ const info = extractComponentInfo(relPath);
538
+ const componentName = info.names.find((n) => n.toLowerCase() === path4.basename(relPath, ".ts").toLowerCase()) ?? info.names[0] ?? path4.basename(relPath, ".ts");
539
+ console.log(`
540
+ \u9009\u4E2D: ${relPath}`);
541
+ console.log(`\u8BC6\u522B\u5230\u7684\u5BFC\u51FA: ${info.names.join(", ") || "\uFF08\u6CA1\u627E\u5230 export \u7684\u7EC4\u4EF6\u540D\uFF0C\u4F1A\u7528\u6587\u4EF6\u540D\u515C\u5E95\uFF09"}`);
542
+ console.log(`\u9ED8\u8BA4\u53D6: ${componentName}\uFF08\u4E0D\u5BF9\u7684\u8BDD\u4E0B\u4E00\u6B65\u53EF\u4EE5\u6539\uFF09`);
543
+ if (info.propsBlocks.length) {
544
+ console.log(`Props \u5B9A\u4E49:`);
545
+ for (const b of info.propsBlocks) {
546
+ const names = [...b.body.matchAll(/^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\??:/gm)].map((m) => m[1]);
547
+ console.log(` ${b.typeName} { ${names.join(", ")} }`);
548
+ }
549
+ } else {
550
+ console.log("\u6CA1\u627E\u5230 Props type/interface\uFF08\u53EF\u80FD\u662F\u5185\u8054\u7C7B\u578B\uFF0C\u751F\u6210\u540E\u81EA\u5DF1\u6838\u5BF9\uFF09");
551
+ }
552
+ const nameInput = (await ask(`
553
+ \u4EE3\u7801\u7EC4\u4EF6\u540D [\u56DE\u8F66\u7528 "${componentName}"]: `)).trim();
554
+ const finalName = nameInput || componentName;
555
+ const confirm = (await ask(`
556
+ \u786E\u8BA4\u751F\u6210\u6620\u5C04\u5417\uFF1F\u8F93\u5165 "good" \u786E\u8BA4\uFF0C\u5176\u4ED6\u4EFB\u610F\u952E\u653E\u5F03: `)).trim();
557
+ if (confirm !== "good") {
558
+ console.log("\u653E\u5F03\uFF0C\u56DE\u5230\u5217\u8868\u3002\n");
559
+ const allFiles = listCandidateFiles();
560
+ return pickLoop(schema, rankFiles(allFiles, schema), allFiles, rest, useAi);
561
+ }
562
+ const config = readConfig();
563
+ const importSpecifier = toImportSpecifier(relPath, config.importPaths);
564
+ const propsBlock = info.propsBlocks[0];
565
+ const content = buildMatchedScaffold({
566
+ schema,
567
+ componentFile: relPath,
568
+ componentName: finalName,
569
+ importSpecifier,
570
+ propsBlock
571
+ });
572
+ return writeMappingFile(schema, content, rest, useAi);
573
+ }
574
+ async function writeMappingFile(schema, content, rest, useAi) {
575
+ const safeName = schema.componentName.replace(/[^a-zA-Z0-9]+/g, "") || "Component";
576
+ const defaultOut = path4.relative(ROOT, path4.join(MAPPINGS_DIR, `${safeName}.figma.ts`));
577
+ const outInput = (await ask(`\u4FDD\u5B58\u5230\u54EA [\u56DE\u8F66\u7528 "${defaultOut}"]: `)).trim();
578
+ const outRel = outInput || defaultOut;
579
+ const outAbs = path4.resolve(ROOT, outRel);
580
+ if (!outAbs.startsWith(ROOT + path4.sep)) {
581
+ console.log(`\u62D2\u7EDD\u5199\u5165\uFF1A${outAbs} \u5728\u9879\u76EE\u6839\u76EE\u5F55\u4E4B\u5916\uFF0C\u8FD9\u4E2A\u811A\u672C\u53EA\u5141\u8BB8\u5F80 ${ROOT} \u91CC\u5199\u3002\u8DF3\u8FC7\u8FD9\u4E2A\u7EC4\u4EF6\u3002
582
+ `);
583
+ return processQueue(rest, useAi);
584
+ }
585
+ if (fs4.existsSync(outAbs)) {
586
+ const overwrite = (await ask(`${outRel} \u5DF2\u5B58\u5728\uFF0C\u8986\u76D6\uFF1F(y/N): `)).trim().toLowerCase();
587
+ if (overwrite !== "y") {
588
+ console.log("\u53D6\u6D88\u5199\u5165\uFF0C\u8DF3\u8FC7\u8FD9\u4E2A\u7EC4\u4EF6\u3002\n");
589
+ return processQueue(rest, useAi);
590
+ }
591
+ }
592
+ fs4.mkdirSync(path4.dirname(outAbs), { recursive: true });
593
+ fs4.writeFileSync(outAbs, content);
594
+ console.log(`
595
+ \u5DF2\u5199\u5165 ${outRel}`);
596
+ const { count } = refreshRegistry({ cwd: ROOT });
597
+ console.log(`registry.generated.ts \u5DF2\u91CD\u65B0\u751F\u6210\uFF08${count} \u6761\u6620\u5C04\uFF09`);
598
+ return processQueue(rest, useAi);
599
+ }
600
+ var args = process.argv.slice(2);
601
+ var useAiFlag = args.includes("--ai");
602
+ var schemaPath = args.find((a) => !a.startsWith("--"));
603
+ if (!schemaPath) {
604
+ printHelp();
605
+ rl.close();
606
+ } else {
607
+ await main(schemaPath, useAiFlag);
608
+ console.log(`
609
+ \u5168\u90E8\u8DD1\u5B8C\uFF0C\u56DE\u5230\u9879\u76EE\u91CC\u8DD1\u4E00\u904D\u7C7B\u578B\u68C0\u67E5 + \u63D2\u4EF6\u6784\u5EFA\uFF08\u6BD4\u5982 rexy \u662F "pnpm run figma:build"\uFF09\uFF0C\u786E\u8BA4\u7F16\u5F97\u8FC7\uFF0C\u518D\u56DE Figma \u91CD\u65B0\u8BFB\u53D6\u9A8C\u8BC1\u3002`);
610
+ rl.close();
611
+ }