mini-figma-code-connect 0.1.1 → 0.1.4

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 +14 -3
  3. package/dist/build.mjs +228 -0
  4. package/dist/figma-mapping.mjs +618 -0
  5. package/dist/generate-registry.mjs +86 -0
  6. package/dist/install-skill.mjs +91 -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 +87 -0
  33. package/dist/scaffold-plugin.mjs +468 -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
package/dist/ui/ui.js ADDED
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ const body = document.getElementById('body');
3
+ const send = (type, extra) => parent.postMessage({ pluginMessage: { type, ...extra } }, '*');
4
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
5
+ function schemaTable(schema, calls) {
6
+ const consumed = new Set(calls.filter((c) => c.depth === 0).map((c) => c.prop));
7
+ const rows = schema.properties
8
+ .map((p) => {
9
+ const tag = consumed.has(p.name)
10
+ ? '<span class="tag ok">已映射</span>'
11
+ : '<span class="tag miss">未映射</span>';
12
+ const opts = p.variantOptions ? esc(p.variantOptions.join(' / ')) : '—';
13
+ return `<tr><td>${esc(p.name)}</td><td><code>${p.type}</code></td><td>${opts}</td><td>${tag}</td></tr>`;
14
+ })
15
+ .join('');
16
+ return `<table><thead><tr><th>属性</th><th>类型</th><th>可选值</th><th></th></tr></thead><tbody>${rows}</tbody></table>`;
17
+ }
18
+ function backBar(candidates) {
19
+ if (!candidates || candidates.length === 0)
20
+ return '';
21
+ return `<div class="row"><button class="ghost back" type="button">← 返回列表(${candidates.length} 个实例)</button></div>`;
22
+ }
23
+ function wireBack() {
24
+ const btn = document.querySelector('.back');
25
+ if (btn)
26
+ btn.addEventListener('click', () => send('backToList'));
27
+ }
28
+ // 一个按钮身兼两职:候选列表状态(选中的是容器,没有单个实例)导出全部;其它状态导出当前选中的单个实例
29
+ const exportSchemaBtn = document.getElementById('export-schema');
30
+ let inCandidatesMode = false;
31
+ function syncExportButton(state, count) {
32
+ inCandidatesMode = state === 'candidates';
33
+ if (state === 'candidates') {
34
+ exportSchemaBtn.disabled = count === 0;
35
+ exportSchemaBtn.textContent = count > 0 ? `导出全部 schema.json(${count} 个)` : '导出全部 schema.json';
36
+ }
37
+ else {
38
+ exportSchemaBtn.disabled = false;
39
+ exportSchemaBtn.textContent = '导出 schema.json';
40
+ }
41
+ }
42
+ function render(msg) {
43
+ if (msg.type !== 'download') {
44
+ syncExportButton(msg.state, msg.state === 'candidates' ? msg.candidates.length : 0);
45
+ }
46
+ if (msg.type === 'download') {
47
+ const url = URL.createObjectURL(new Blob([msg.json], { type: 'application/json' }));
48
+ const a = document.createElement('a');
49
+ a.href = url;
50
+ a.download = msg.filename;
51
+ document.body.appendChild(a);
52
+ a.click();
53
+ a.remove();
54
+ URL.revokeObjectURL(url);
55
+ // 兜底:iframe 的 sandbox 若拦掉下载,至少把内容留在剪贴板里
56
+ void navigator.clipboard.writeText(msg.json).catch(() => undefined);
57
+ return;
58
+ }
59
+ if (msg.state === 'scan') {
60
+ // 未映射排前面(跟 scanPage 排序一致),仿 Figma 官方 Code Connect UI 那张组件目录列表的样式:
61
+ // 一行一个组件,左边 Figma 组件名,右边状态 + 代码组件名,顶部一个搜索框过滤
62
+ const rows = msg.groups
63
+ .map((g) => `
64
+ <li class="component-row" data-search="${esc(g.componentName.toLowerCase())}">
65
+ <button class="row-name" type="button" data-id="${esc(g.sampleNodeId)}">
66
+ <span class="diamond">◆</span>
67
+ <span>${esc(g.componentName)}</span>
68
+ <span class="dim">${g.count} 处</span>
69
+ </button>
70
+ <span class="row-code">
71
+ ${g.mapped
72
+ ? `<span class="status-dot ok">✓</span><code>&lt;${esc(g.codeComponent ?? '?')}&gt;</code>`
73
+ : `<span class="status-dot miss">×</span><span class="dim">未映射</span>`}
74
+ </span>
75
+ </li>`)
76
+ .join('');
77
+ body.innerHTML = `
78
+ <section>
79
+ <p class="hint">${esc(msg.message)}</p>
80
+ <input class="scan-search" type="text" placeholder="搜索组件…" />
81
+ <div class="component-list">
82
+ <div class="list-header"><span>Figma 组件</span><span>代码组件</span></div>
83
+ <ul class="component-rows">${rows}</ul>
84
+ </div>
85
+ </section>
86
+ `;
87
+ body.querySelectorAll('.row-name').forEach((el) => {
88
+ el.addEventListener('click', () => send('pick', { id: el.dataset.id }));
89
+ });
90
+ const search = document.querySelector('.scan-search');
91
+ if (search) {
92
+ search.addEventListener('input', () => {
93
+ const q = search.value.trim().toLowerCase();
94
+ body.querySelectorAll('.component-row').forEach((row) => {
95
+ row.style.display = !q || (row.dataset.search ?? '').includes(q) ? '' : 'none';
96
+ });
97
+ });
98
+ }
99
+ return;
100
+ }
101
+ if (msg.state === 'candidates') {
102
+ const items = msg.candidates
103
+ .map((c, i) => `<li><button class="ghost pick" type="button" data-id="${esc(c.id)}"><span>${i + 1}. ${esc(c.name)}</span><span class="tag ${c.mapped ? 'ok' : 'miss'}">${c.mapped ? '已映射' : '未映射'}</span></button></li>`)
104
+ .join('');
105
+ body.innerHTML = `<section><p class="hint">${esc(msg.message)}</p><ul class="candidates">${items}</ul></section>`;
106
+ body.querySelectorAll('.pick').forEach((btn) => {
107
+ btn.addEventListener('click', () => send('pick', { id: btn.dataset.id }));
108
+ });
109
+ return;
110
+ }
111
+ if (msg.state !== 'ok') {
112
+ const extra = msg.schema && msg.schema.properties.length > 0
113
+ ? `<section><h2>读到的属性 schema</h2>${schemaTable(msg.schema, [])}</section>`
114
+ : '';
115
+ // 未映射可能是因为压根没有对应的代码组件——给一条能直接丢进 AI 工具的提示词,
116
+ // 先把组件生成出来(组件名跟 Figma 实例名保持一致,方便后面接 mini-code-connect 映射)
117
+ const genPrompt = msg.state === 'unmapped' && msg.schema && msg.figmaUrl
118
+ ? `<section>
119
+ <div class="row"><h2>生成通用组件</h2><button class="ghost copy" id="copy-prompt" type="button">复制</button></div>
120
+ <pre id="gen-prompt">/figma-design-to-code ${esc(msg.figmaUrl)},生成通用组件"${esc(msg.schema.componentName)}",组件名和实例名保持一致</pre>
121
+ </section>`
122
+ : '';
123
+ body.innerHTML = `${backBar(msg.candidates)}<section><p class="hint">${esc(msg.message)}</p></section>${genPrompt}${extra}`;
124
+ wireBack();
125
+ const copyPrompt = document.getElementById('copy-prompt');
126
+ if (copyPrompt) {
127
+ copyPrompt.addEventListener('click', () => {
128
+ const pre = document.getElementById('gen-prompt');
129
+ if (!pre || !pre.textContent)
130
+ return;
131
+ void navigator.clipboard.writeText(pre.textContent).then(() => {
132
+ copyPrompt.textContent = '已复制';
133
+ setTimeout(() => (copyPrompt.textContent = '复制'), 1200);
134
+ });
135
+ });
136
+ }
137
+ return;
138
+ }
139
+ const { schema, template, snippet, imports, findings, calls } = msg;
140
+ const bindingCard = `
141
+ <div class="card">
142
+ <div class="kv"><span>Figma 组件</span><span>${esc(schema.componentName)}</span></div>
143
+ <div class="kv"><span>source</span><span><code>${esc(template.meta.source)}</code></span></div>
144
+ <div class="kv"><span>component</span><span><code>${esc(template.meta.component)}</code></span></div>
145
+ <div class="kv"><span>template id</span><span><code>${esc(template.id)}</code></span></div>
146
+ </div>`;
147
+ const findingList = findings.length === 0
148
+ ? '<p class="hint">没有发现问题。</p>'
149
+ : `<ul class="findings">${findings
150
+ .map((f) => `<li class="${f.level}">${esc(f.text)}</li>`)
151
+ .join('')}</ul>`;
152
+ const importLines = imports.length ? esc(imports.join('\n')) + '\n\n' : '';
153
+ body.innerHTML = `
154
+ ${backBar(msg.candidates)}
155
+ <section><h2>绑定</h2>${bindingCard}</section>
156
+ <section>
157
+ <div class="row"><h2>生成的代码片段</h2><button class="ghost copy" id="copy" type="button">复制</button></div>
158
+ <pre id="snippet">${importLines}${esc(snippet)}</pre>
159
+ </section>
160
+ <section><h2>属性 schema 与覆盖率</h2>${schemaTable(schema, calls)}</section>
161
+ <section><h2>自检(Step 6)</h2>${findingList}</section>
162
+ <section><h2>本次 accessor 调用</h2><pre>${esc(calls
163
+ .map((c) => `${' '.repeat(c.depth)}${c.method}("${c.prop}")${c.ok ? '' : ` ⚠ ${c.note ?? ''}`}`)
164
+ .join('\n') || '(无)')}</pre></section>`;
165
+ const copy = document.getElementById('copy');
166
+ if (copy) {
167
+ copy.addEventListener('click', () => {
168
+ const pre = document.getElementById('snippet');
169
+ if (!pre || !pre.textContent)
170
+ return;
171
+ void navigator.clipboard.writeText(pre.textContent).then(() => {
172
+ copy.textContent = '已复制';
173
+ setTimeout(() => (copy.textContent = '复制'), 1200);
174
+ });
175
+ });
176
+ }
177
+ wireBack();
178
+ }
179
+ ;
180
+ document.getElementById('refresh').addEventListener('click', () => send('refresh'));
181
+ document.getElementById('scan').addEventListener('click', () => send('scan'));
182
+ exportSchemaBtn.addEventListener('click', () => send(inCandidatesMode ? 'exportAllSchemas' : 'exportSchema'));
183
+ document.getElementById('export-mapping-table').addEventListener('click', () => send('exportMappingTable'));
184
+ onmessage = (e) => {
185
+ const msg = e.data && e.data.pluginMessage;
186
+ if (msg)
187
+ render(msg);
188
+ };
189
+ // UI 就绪后主动拉一次。主线程不做首次 post —— 那会早于这行代码执行,首帧会丢
190
+ send('refresh');
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "mini-figma-code-connect",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Local Code Connect for Figma Professional — 无 Org 也能做确定性的 f(instance) → snippet。",
6
- "license": "MIT",
6
+ "license": "UNLICENSED",
7
7
  "author": "elaine.ma <elaine.ma@aspendigital.co>",
8
8
  "repository": {
9
9
  "type": "git",
@@ -24,32 +24,35 @@
24
24
  "node": ">=18"
25
25
  },
26
26
  "files": [
27
- "src/**/*.ts",
28
- "src/**/*.html",
29
- "scripts",
27
+ "dist",
30
28
  ".claude/skills/mini-code-connect",
31
29
  "figma-mapping.config.example.json",
32
30
  "LICENSE",
33
31
  "README.md"
34
32
  ],
33
+ "types": "./dist/runtime/index.d.ts",
35
34
  "exports": {
36
- ".": "./src/runtime/index.ts",
37
- "./build": "./scripts/build-plugin.mjs",
38
- "./generate-registry": "./scripts/generate-registry.mjs",
39
- "./install-skill": "./scripts/install-skill.mjs",
40
- "./scaffold-manifest": "./scripts/scaffold-manifest.mjs",
41
- "./scaffold-plugin": "./scripts/scaffold-plugin.mjs"
35
+ ".": {
36
+ "types": "./dist/runtime/index.d.ts",
37
+ "import": "./dist/runtime/index.js"
38
+ },
39
+ "./build": "./dist/build.mjs",
40
+ "./generate-registry": "./dist/generate-registry.mjs",
41
+ "./install-skill": "./dist/install-skill.mjs",
42
+ "./scaffold-manifest": "./dist/scaffold-manifest.mjs",
43
+ "./scaffold-plugin": "./dist/scaffold-plugin.mjs"
42
44
  },
43
45
  "bin": {
44
- "figma-mapping": "scripts/figma-mapping/index.mjs",
45
- "figma-code-connect-build": "scripts/build-plugin.mjs",
46
- "mini-code-connect-install-skill": "scripts/install-skill.mjs",
47
- "mini-code-connect-scaffold-manifest": "scripts/scaffold-manifest.mjs",
48
- "mini-code-connect-scaffold-plugin": "scripts/scaffold-plugin.mjs"
46
+ "figma-mapping": "dist/figma-mapping.mjs",
47
+ "figma-code-connect-build": "dist/build.mjs",
48
+ "mini-code-connect-install-skill": "dist/install-skill.mjs",
49
+ "mini-code-connect-scaffold-manifest": "dist/scaffold-manifest.mjs",
50
+ "mini-code-connect-scaffold-plugin": "dist/scaffold-plugin.mjs"
49
51
  },
50
52
  "scripts": {
51
- "build": "node esbuild.config.mjs",
52
- "watch": "node esbuild.config.mjs --watch",
53
+ "build": "node scripts/build-package.mjs",
54
+ "build:demo-plugin": "npm run build && node esbuild.config.mjs",
55
+ "watch": "npm run build && node esbuild.config.mjs --watch",
53
56
  "pretypecheck": "npm run generate:registry && npm run generate:mapping-table",
54
57
  "typecheck": "tsc --noEmit",
55
58
  "generate:registry": "node scripts/generate-registry.mjs 'figma-mappings/**/*.figma.ts' src/runtime/registry.generated.ts",
@@ -57,6 +60,8 @@
57
60
  "demo": "esbuild src/dev/demo.ts --bundle --platform=node --format=esm --outfile=dist/demo.mjs --log-level=warning && node dist/demo.mjs",
58
61
  "preexport": "npm run generate:registry",
59
62
  "export": "esbuild src/dev/export.ts --bundle --platform=node --format=esm --outfile=dist/export.mjs --log-level=warning && node dist/export.mjs",
63
+ "sync:public": "npm run build && node scripts/sync-public.mjs",
64
+ "release:public": "node scripts/release-public.mjs",
60
65
  "prepublishOnly": "npm run build"
61
66
  },
62
67
  "dependencies": {
@@ -1,132 +0,0 @@
1
- #!/usr/bin/env node
2
- import * as esbuild from 'esbuild';
3
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { isCliEntrypoint } from './is-cli-entrypoint.mjs';
7
- import { generateRegistry } from './generate-registry.mjs';
8
- import { copyMappingTable } from './copy-mapping-table.mjs';
9
-
10
- const PACKAGE_ROOT = path.resolve(fileURLToPath(import.meta.url), '../..');
11
- const MAIN_ENTRY = path.join(PACKAGE_ROOT, 'src/main/code.ts');
12
- const UI_ENTRY = path.join(PACKAGE_ROOT, 'src/ui/ui.ts');
13
- const UI_HTML = path.join(PACKAGE_ROOT, 'src/ui/ui.html');
14
-
15
- /**
16
- * 把生成文件写到消费方 outDir/.generated/,再用 esbuild 插件把源码里的
17
- * `./registry.generated` / `./mapping-table.generated.json` 指过去。
18
- * 不能再写进包自己的 src/:npm/pnpm 装到 node_modules 后该目录只读或被多项目共享。
19
- */
20
- function consumerGeneratedPlugin(genDir) {
21
- const registryFile = path.join(genDir, 'registry.generated.ts');
22
- const mappingTableFile = path.join(genDir, 'mapping-table.generated.json');
23
- return {
24
- name: 'consumer-generated',
25
- setup(build) {
26
- build.onResolve({ filter: /(?:^|[\\/])registry\.generated(?:\.ts)?$/ }, () => ({
27
- path: registryFile,
28
- }));
29
- build.onResolve({ filter: /(?:^|[\\/])mapping-table\.generated\.json$/ }, () => ({
30
- path: mappingTableFile,
31
- }));
32
- },
33
- };
34
- }
35
-
36
- /**
37
- * 把这套 Code Connect 插件引擎打包成消费方能装进 Figma 的 dist/code.js + dist/ui.html。
38
- *
39
- * @param {object} opts
40
- * @param {string} opts.cwd 消费方项目根目录(.figma.ts 文件按这个目录算相对路径)
41
- * @param {string} opts.mappingsGlob 相对 cwd 的 glob,例如 'mappings/**\/*.figma.ts'
42
- * @param {string} [opts.mappingTablePath] 相对 cwd 的 figma-mapping-table.json 路径,
43
- * 用于插件面板"下载 mapping-table.json"按钮(下载文件名仍为 mapping-table.json);不存在时下载空数组
44
- * @param {string} [opts.outDir] 相对 cwd 的产物目录,默认 'dist'
45
- * @param {boolean} [opts.watch] watch 模式
46
- */
47
- export async function buildPlugin({
48
- cwd,
49
- mappingsGlob,
50
- mappingTablePath = 'figma-mapping-table.json',
51
- outDir = 'dist',
52
- watch = false,
53
- }) {
54
- const absOutDir = path.resolve(cwd, outDir);
55
- const genDir = path.join(absOutDir, '.generated');
56
- const registryOut = path.join(genDir, 'registry.generated.ts');
57
- const mappingTableOut = path.join(genDir, 'mapping-table.generated.json');
58
-
59
- const { count } = generateRegistry({
60
- cwd,
61
- mappingsGlob,
62
- outFile: registryOut,
63
- typesImport: null,
64
- });
65
- console.log(`[build-plugin] 扫到 ${count} 条映射(${mappingsGlob})`);
66
- const { found } = copyMappingTable({ cwd, mappingTablePath, outFile: mappingTableOut });
67
- if (!found) console.log(`[build-plugin] 没找到 ${mappingTablePath},下载按钮会给空数组`);
68
-
69
- const generatedPlugin = consumerGeneratedPlugin(genDir);
70
-
71
- const inlineUi = {
72
- name: 'inline-ui',
73
- setup(build) {
74
- build.onEnd(async (res) => {
75
- if (res.errors.length) return;
76
- const js = res.outputFiles
77
- ? res.outputFiles[0].text
78
- : await readFile(path.join(absOutDir, '.ui.tmp.js'), 'utf8');
79
- const html = await readFile(UI_HTML, 'utf8');
80
- await mkdir(absOutDir, { recursive: true });
81
- await writeFile(path.join(absOutDir, 'ui.html'), html.replace('/*__UI_JS__*/', () => js));
82
- console.log(`[inline-ui] ${path.join(outDir, 'ui.html')} 已更新`);
83
- });
84
- },
85
- };
86
-
87
- const codeOpts = {
88
- entryPoints: [MAIN_ENTRY],
89
- bundle: true,
90
- format: 'iife',
91
- target: 'es2020',
92
- outfile: path.join(absOutDir, 'code.js'),
93
- logLevel: 'info',
94
- plugins: [generatedPlugin],
95
- };
96
-
97
- const uiOpts = {
98
- entryPoints: [UI_ENTRY],
99
- bundle: true,
100
- format: 'iife',
101
- target: 'es2020',
102
- outfile: path.join(absOutDir, '.ui.tmp.js'),
103
- logLevel: 'warning',
104
- plugins: [inlineUi, generatedPlugin],
105
- };
106
-
107
- if (watch) {
108
- const a = await esbuild.context(codeOpts);
109
- const b = await esbuild.context(uiOpts);
110
- await Promise.all([a.watch(), b.watch()]);
111
- console.log('[build-plugin] watching...');
112
- return { watching: true };
113
- }
114
-
115
- await esbuild.build(codeOpts);
116
- await esbuild.build(uiOpts);
117
- return { watching: false };
118
- }
119
-
120
- // 允许直接当 CLI 跑:node scripts/build-plugin.mjs <mappingsGlob> [--out dist] [--watch]
121
- if (isCliEntrypoint(import.meta.url)) {
122
- const args = process.argv.slice(2);
123
- const watch = args.includes('--watch');
124
- const outIdx = args.indexOf('--out');
125
- const outDir = outIdx !== -1 ? args[outIdx + 1] : 'dist';
126
- const mappingsGlob = args.find((a, i) => !a.startsWith('--') && args[i - 1] !== '--out');
127
- if (!mappingsGlob) {
128
- console.error('用法: node scripts/build-plugin.mjs <mappingsGlob> [--out dist] [--watch]');
129
- process.exit(1);
130
- }
131
- await buildPlugin({ cwd: process.cwd(), mappingsGlob, outDir, watch });
132
- }
@@ -1,19 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- /**
5
- * 把消费方的 figma-mapping-table.json 复制到指定 outFile,供 code.ts 经 esbuild
6
- * 插件静态 import。默认由 build-plugin 写到消费方 outDir/.generated/,不再写进包的 src/。
7
- *
8
- * @param {object} opts
9
- * @param {string} opts.cwd 消费方项目根目录
10
- * @param {string} opts.mappingTablePath 相对 cwd 的映射表路径(默认 figma-mapping-table.json);不存在时写一个空数组
11
- * @param {string} opts.outFile 生成文件的绝对路径
12
- */
13
- export function copyMappingTable({ cwd, mappingTablePath, outFile }) {
14
- const src = path.resolve(cwd, mappingTablePath);
15
- const content = fs.existsSync(src) ? fs.readFileSync(src, 'utf8') : '[]\n';
16
- fs.mkdirSync(path.dirname(outFile), { recursive: true });
17
- fs.writeFileSync(outFile, content);
18
- return { found: fs.existsSync(src) };
19
- }
@@ -1,102 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
-
3
- // 引擎自带的格式示例,不依赖任何具体消费方的真实数据——这个 CLI 可能在任何装了
4
- // mini-figma-code-connect 的项目里跑,不能假设某份 .figma.ts 一定存在。
5
- const EXAMPLE_TEMPLATE = `import { defineTemplate, code } from 'mini-figma-code-connect'
6
-
7
- export default defineTemplate({
8
- meta: {
9
- url: 'https://www.figma.com/design/FILE_KEY/Design-System?node-id=NODE_ID',
10
- source: '../path/to/components/button.tsx',
11
- component: 'Button',
12
- },
13
- id: 'button',
14
- match: { componentName: 'Button' },
15
-
16
- render(instance) {
17
- const label = instance.getString('Label')
18
- const disabled = instance.getBoolean('Disabled')
19
- const size = instance.getEnum('Size', { Large: 'large', Medium: 'medium', Small: 'small' })
20
-
21
- return {
22
- example: code\`<Button size="\${size}"\${disabled ? code\` disabled\` : ''}>\${label}</Button>\`,
23
- imports: ['import { Button } from "@/components/button"'],
24
- id: 'button',
25
- }
26
- },
27
- })
28
- `;
29
-
30
- function buildPrompt({ schema, candidates, importPaths }) {
31
- const candidateBlocks = candidates
32
- .map(
33
- (c) => `### 候选文件: ${c.relPath}
34
- \`\`\`
35
- ${c.source}
36
- \`\`\`
37
- `,
38
- )
39
- .join('\n');
40
-
41
- return `你是 Figma Code Connect 映射生成器。任务:判断下面这个 Figma 组件该接到哪个候选代码组件,如果有合适的就生成映射文件内容。
42
-
43
- ## Figma 组件 schema
44
- \`\`\`json
45
- ${JSON.stringify(schema, null, 2)}
46
- \`\`\`
47
-
48
- ## 候选代码组件(按路径排列,可能一个都不合适)
49
- ${candidateBlocks}
50
-
51
- ## 目标文件格式(这是一份已有的示例,照这个格式和 accessor API 写,不要用别的格式)
52
- \`\`\`ts
53
- ${EXAMPLE_TEMPLATE}
54
- \`\`\`
55
-
56
- ## import 路径换算规则
57
- 候选文件路径前缀 → 生成文件里 imports 用的 specifier:
58
- ${JSON.stringify(importPaths, null, 2)}
59
-
60
- ## 要求
61
- 1. 从候选文件里选一个最合适的(组件名、Props 语义都要对得上,不是字符串像不像)。一个都不合适就选 NONE。
62
- 2. Figma 属性名可能是中文、可能跟代码 prop 不是字面一致(比如"状态"里的 disabled/hover/loading 需要拆成 disabled/loading 两个 prop,hover/pressed 是 CSS 状态不是 prop,不能硬塞)。VARIANT 的字典要覆盖 schema 里给的每一个选项,配不上真实值的必须显式留 'TODO' 不要瞎编一个能编译但语义错的值。
63
- 3. 不确定的判断(比如没有精确对应的 variant 值该猜哪个),照样做出选择,但在生成文件顶部注释里明确写出"这是猜的,需要人工确认",别不声不响。
64
- 4. 严禁调用任何工具/命令,严禁读写文件系统 —— 你现在没有这些权限,只需要基于上面给的信息直接生成文本。
65
- 5. 严禁编造代码里不存在的 prop 名。
66
-
67
- ## 输出格式(严格遵守,不要输出其它任何文字)
68
- 第一行:\`MATCH: <候选文件的 relPath>\` 或 \`MATCH: NONE\`
69
- 如果 MATCH 不是 NONE,第二行开始是一条分隔线 \`---\`,之后是完整的 .figma.ts 文件内容(不要用 markdown 代码块包裹,直接是可以原样写入文件的 TypeScript 源码)。
70
- 如果 MATCH 是 NONE,第二行开始简短说明为什么没有合适的候选(一两句话)。`;
71
- }
72
-
73
- /** 调本机 claude CLI 无头模式做真判断,禁掉所有工具权限强制纯文本生成,不给它碰文件系统的机会 */
74
- export function aiGenerateMapping({ schema, candidates, importPaths }) {
75
- const prompt = buildPrompt({ schema, candidates, importPaths });
76
-
77
- const raw = execFileSync(
78
- 'claude',
79
- [
80
- '-p',
81
- prompt,
82
- '--output-format',
83
- 'text',
84
- '--disallowedTools',
85
- 'Read,Write,Edit,Bash,Glob,Grep,WebFetch,WebSearch,Task,NotebookEdit',
86
- ],
87
- { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 },
88
- );
89
-
90
- const firstLineEnd = raw.indexOf('\n');
91
- const firstLine = (firstLineEnd === -1 ? raw : raw.slice(0, firstLineEnd)).trim();
92
- const rest = firstLineEnd === -1 ? '' : raw.slice(firstLineEnd + 1);
93
-
94
- const m = firstLine.match(/^MATCH:\s*(.+)$/);
95
- if (!m || m[1].trim() === 'NONE') {
96
- return { matched: false, reason: rest.trim() || firstLine };
97
- }
98
-
99
- const relPath = m[1].trim();
100
- const content = rest.replace(/^---\s*\n?/, '');
101
- return { matched: true, relPath, content };
102
- }