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
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { isCliEntrypoint } from './is-cli-entrypoint.mjs';
6
-
7
- const PACKAGE_ROOT = path.resolve(fileURLToPath(import.meta.url), '../..');
8
- const SKILL_SOURCE = path.join(PACKAGE_ROOT, '.claude/skills/mini-code-connect');
9
- const SKILL_NAME = 'mini-code-connect';
10
-
11
- /**
12
- * `.claude/`、`.agents/` 这些约定都是挂在 git 仓库根目录的,不是随便哪个 cwd。
13
- * pnpm monorepo 里这个包大概率是某个子包(比如 apps/rexy)的 devDependency,
14
- * 从那跑命令 cwd 就是子包目录,不是仓库根——得从 cwd 往上找到 .git 所在的那一层。
15
- * 找不到 .git(比如根本不在 git 仓库里)就退回用 cwd 本身,不报错。
16
- */
17
- function findRepoRoot(cwd) {
18
- let dir = cwd;
19
- while (true) {
20
- if (fs.existsSync(path.join(dir, '.git'))) return dir;
21
- const parent = path.dirname(dir);
22
- if (parent === dir) return cwd; // 到根了还没找到 .git,放弃,退回 cwd
23
- dir = parent;
24
- }
25
- }
26
-
27
- /**
28
- * 把这个包自带的 mini-code-connect skill 复制进消费方项目——Claude Code 只认消费方
29
- * 项目自己 `.claude/skills/` 下的文件,装了这个 npm 包不代表 skill 会被发现,
30
- * 得实际把 SKILL.md 落到消费方项目里才能用 /mini-code-connect 或自然语言触发。
31
- *
32
- * 默认复制进 `.claude/skills/`(Claude Code 自己认的位置);如果项目根目录已经有
33
- * `.agents/skills/`(多 agent 工具共享 skill 的常见约定,比如 trex-website 就是这样),
34
- * 额外复制一份进去——不强求消费方一定要有这个目录,检测到才复制。两份都写是因为不确定
35
- * 消费方是否有自动把 `.agents/skills/` 同步进 `.claude/skills/` 的机制;如果消费方项目
36
- * 自己已经有这类同步工具,`.claude/skills/` 这份可以不用管(or 加 opts.claudeMirror: false
37
- * 关掉,避免跟同步工具打架)。
38
- *
39
- * @param {object} opts
40
- * @param {string} opts.cwd 运行这个命令时所在的目录(子包 cwd 也行,会自动找仓库根)
41
- * @param {string} [opts.root] 显式指定目标根目录,跳过自动探测 .git
42
- * @param {boolean} [opts.force] 目标已存在时是否覆盖,默认不覆盖(避免吃掉消费方自己的改动)
43
- * @param {boolean} [opts.claudeMirror] 是否也写一份进 `.claude/skills/`,默认 true
44
- */
45
- export function installSkill({ cwd, root, force = false, claudeMirror = true }) {
46
- const base = root ?? findRepoRoot(cwd);
47
- const targets = [];
48
- if (claudeMirror) targets.push(path.join(base, '.claude/skills', SKILL_NAME));
49
- if (fs.existsSync(path.join(base, '.agents/skills'))) {
50
- targets.push(path.join(base, '.agents/skills', SKILL_NAME));
51
- }
52
-
53
- const results = [];
54
- for (const target of targets) {
55
- if (fs.existsSync(target) && !force) {
56
- results.push({ target, skipped: true });
57
- continue;
58
- }
59
- fs.rmSync(target, { recursive: true, force: true });
60
- fs.cpSync(SKILL_SOURCE, target, { recursive: true });
61
- results.push({ target, skipped: false });
62
- }
63
- return { results, root: base };
64
- }
65
-
66
- if (isCliEntrypoint(import.meta.url)) {
67
- const args = process.argv.slice(2);
68
- const force = args.includes('--force');
69
- const noClaudeMirror = args.includes('--no-claude-mirror');
70
- const rootIdx = args.indexOf('--root');
71
- const root = rootIdx !== -1 ? path.resolve(args[rootIdx + 1]) : undefined;
72
-
73
- const { results, root: usedRoot } = installSkill({ cwd: process.cwd(), root, force, claudeMirror: !noClaudeMirror });
74
- console.log(`[install-skill] 项目根目录:${usedRoot}`);
75
- for (const r of results) {
76
- console.log(r.skipped ? `[install-skill] 已存在,跳过(加 --force 覆盖):${r.target}` : `[install-skill] 已写入:${r.target}`);
77
- }
78
- console.log('\n跑 /mini-code-connect(或直接说"帮我映射这个 Figma 组件")触发 skill。');
79
- }
@@ -1,21 +0,0 @@
1
- import { realpathSync } from 'node:fs';
2
- import { fileURLToPath } from 'node:url';
3
-
4
- /**
5
- * "这个模块是不是被直接当 CLI 跑的(而不是被 import)" 的正确判断。
6
- *
7
- * `import.meta.url === \`file://${process.argv[1]}\`` 这种写法几乎总是 false:
8
- * process.argv[1] 是命令行原样传入的路径(常见是相对路径,比如 `node scripts/x.mjs`
9
- * 或经 pnpm bin 软链调用时是 `node_modules/pkg/scripts/x.mjs`),既没转绝对路径也没解析
10
- * 符号链接;import.meta.url 则永远是绝对路径,且经过符号链接解析(这个包大概率是通过
11
- * `link:` 装的,实际文件在别处)。两边格式对不上,条件永远不成立,CLI 分支静默不执行。
12
- * 用 realpathSync 把两边都归一化到真实绝对路径再比较。
13
- */
14
- export function isCliEntrypoint(importMetaUrl) {
15
- if (!process.argv[1]) return false;
16
- try {
17
- return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(importMetaUrl));
18
- } catch {
19
- return false; // argv[1] 不是个真实文件路径(比如 REPL/-e)时 realpathSync 会抛
20
- }
21
- }
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { isCliEntrypoint } from './is-cli-entrypoint.mjs';
5
-
6
- /**
7
- * 生成消费方项目自己的 manifest.json + manifest.codegen.json —— 两份 Figma 插件清单,
8
- * `main`/`ui` 指向消费方自己 buildPlugin() 的产物目录(outDir 得跟 buildPlugin() 调用时传的
9
- * 一致,两边各自维护,这个函数不做校验)。已存在的文件不覆盖,避免吃掉消费方自己加的字段
10
- * (比如 codegenLanguages 加了更多语言)——要重新生成就先手动删掉旧文件。
11
- *
12
- * 两份清单必须分开:官方 codegen 插件要求 editorType 只能是 ["dev"],跟 "figma"/"inspect"
13
- * 写一起 Figma 会报错。
14
- *
15
- * @param {object} opts
16
- * @param {string} opts.cwd 消费方项目根目录,manifest 写在这里
17
- * @param {string} [opts.name] 面板插件在 Figma 里显示的名字,默认 'Mini Code Connect'——
18
- * Figma 账号里同时装了多个消费方的插件时会撞名(区分靠 id,不靠 name),需要区分再传
19
- * @param {string} opts.id 面板插件的 manifest id,同一账号内必须唯一,例如 'trex-rexy-code-connect'
20
- * @param {string} [opts.outDir] 相对 cwd 的构建产物目录,须跟 buildPlugin() 的 outDir 一致,默认 'dist'
21
- */
22
- export function scaffoldManifest({ cwd, name = 'Mini Code Connect', id, outDir = 'dist' }) {
23
- if (!id) throw new Error('scaffoldManifest: id 必填');
24
-
25
- const panel = {
26
- name,
27
- id,
28
- api: '1.0.0',
29
- main: `${outDir}/code.js`,
30
- ui: `${outDir}/ui.html`,
31
- editorType: ['figma', 'dev'],
32
- capabilities: ['inspect'],
33
- documentAccess: 'dynamic-page',
34
- networkAccess: { allowedDomains: ['none'] },
35
- };
36
- const codegen = {
37
- name: `${name} (Codegen)`,
38
- id: `${id}-codegen`,
39
- api: '1.0.0',
40
- main: `${outDir}/code.js`,
41
- ui: `${outDir}/ui.html`,
42
- editorType: ['dev'],
43
- capabilities: ['codegen'],
44
- codegenLanguages: [{ label: 'React', value: 'react' }],
45
- documentAccess: 'dynamic-page',
46
- networkAccess: { allowedDomains: ['none'] },
47
- };
48
-
49
- const results = [];
50
- for (const [file, content] of [
51
- ['manifest.json', panel],
52
- ['manifest.codegen.json', codegen],
53
- ]) {
54
- const target = path.join(cwd, file);
55
- if (fs.existsSync(target)) {
56
- results.push({ target, skipped: true });
57
- continue;
58
- }
59
- fs.writeFileSync(target, JSON.stringify(content, null, 2) + '\n');
60
- results.push({ target, skipped: false });
61
- }
62
- return { results };
63
- }
64
-
65
- if (isCliEntrypoint(import.meta.url)) {
66
- const args = process.argv.slice(2);
67
- const get = (flag) => {
68
- const i = args.indexOf(flag);
69
- return i === -1 ? undefined : args[i + 1];
70
- };
71
- const name = get('--name') ?? 'Mini Code Connect';
72
- const id = get('--id');
73
- const outDir = get('--out') ?? 'dist';
74
-
75
- if (!id) {
76
- console.error('用法: node scripts/scaffold-manifest.mjs --id <manifest-id> [--name "<面板显示名>"] [--out dist]');
77
- process.exit(1);
78
- }
79
-
80
- const { results } = scaffoldManifest({ cwd: process.cwd(), name, id, outDir });
81
- for (const r of results) {
82
- console.log(r.skipped ? `[scaffold-manifest] 已存在,跳过:${r.target}` : `[scaffold-manifest] 已写入:${r.target}`);
83
- }
84
- }
@@ -1,172 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { installSkill } from './install-skill.mjs';
5
- import { scaffoldManifest } from './scaffold-manifest.mjs';
6
- import { buildPlugin } from './build-plugin.mjs';
7
- import { isCliEntrypoint } from './is-cli-entrypoint.mjs';
8
-
9
- /**
10
- * 一条命令做完消费方接入的全部一次性脚手架:skill、候选目录配置、manifest、构建脚本、
11
- * package.json 里的 figma:build/figma:watch、.gitignore,最后自己跑一次 build——跑完就是
12
- * 能直接导入 Figma 的状态,不需要再手动补一次 `pnpm run figma:build`(少一步就少一次"跑完
13
- * 以为完事了,其实产物没生成"的坑)。唯一必须消费方自己敲的只剩:
14
- * `pnpm add -D mini-figma-code-connect` —— 得先装上这个包,这个函数才存在
15
- * 装好真实映射后的后续 build(改完 .figma.ts 要重新打包)继续用 `pnpm run figma:build`/
16
- * `figma:watch`,那是常态操作,不是这个一次性 setup 的一部分。
17
- *
18
- * 幂等:每一步该跳过就跳过(已存在的文件不覆盖,package.json 已有的 script key 不覆盖),
19
- * 重复跑不会把消费方自己后续的修改吃掉;末尾的 build 步骤本身也是幂等操作,可以放心重跑。
20
- *
21
- * @param {object} opts
22
- * @param {string} opts.cwd 消费方子项目根目录(manifest/config/构建脚本都落在这里,比如 apps/rexy)
23
- * @param {string} [opts.name] 插件在 Figma 里显示的名字,默认 'Mini Code Connect'——账号里同时
24
- * 装了多个消费方的插件时会撞名(区分靠 id,不靠 name),需要区分再传
25
- * @param {string} opts.id manifest id,同一账号内必须唯一,例如 'trex-rexy-code-connect'
26
- * @param {string} [opts.outDir] 构建产物目录,默认 'figma-plugin-dist'
27
- * @param {string} [opts.componentsDir] 候选组件目录(相对 cwd),默认 'components'
28
- * @param {string} [opts.mappingsGlob] .figma.ts 的 glob(相对 cwd),默认 'figma-mappings/**\/*.figma.ts'
29
- * @param {string} [opts.mappingTablePath] mapping-table.json 路径(相对 cwd),默认 'figma-mapping-table.json'
30
- * @param {string} [opts.root] 传给 installSkill 的仓库根覆盖,不传就自动探测 .git
31
- * @param {boolean} [opts.force] 已存在的文件/script key 也覆盖
32
- */
33
- export async function scaffoldPlugin({
34
- cwd,
35
- name = 'Mini Code Connect',
36
- id,
37
- outDir = 'figma-plugin-dist',
38
- componentsDir = 'components',
39
- mappingsGlob = 'figma-mappings/**/*.figma.ts',
40
- mappingTablePath = 'figma-mapping-table.json',
41
- root,
42
- force = false,
43
- }) {
44
- if (!id) throw new Error('scaffoldPlugin: id 必填');
45
-
46
- const steps = [];
47
-
48
- const skill = installSkill({ cwd, root, force });
49
- steps.push({ step: 'install-skill', ...skill });
50
-
51
- const configPath = path.join(cwd, 'figma-mapping.config.json');
52
- if (fs.existsSync(configPath) && !force) {
53
- steps.push({ step: 'config', target: configPath, skipped: true });
54
- } else {
55
- const config = {
56
- codeConnect: {
57
- paths: { [path.basename(componentsDir)]: componentsDir },
58
- importPaths: { [`${componentsDir}/*`]: `@/${componentsDir}/*` },
59
- },
60
- };
61
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
62
- steps.push({ step: 'config', target: configPath, skipped: false });
63
- }
64
-
65
- const manifest = scaffoldManifest({ cwd, name, id, outDir });
66
- steps.push({ step: 'manifest', ...manifest });
67
-
68
- const buildScriptPath = path.join(cwd, 'scripts/build-figma-plugin.mjs');
69
- if (fs.existsSync(buildScriptPath) && !force) {
70
- steps.push({ step: 'build-script', target: buildScriptPath, skipped: true });
71
- } else {
72
- const buildScript = `#!/usr/bin/env node
73
- import { buildPlugin } from 'mini-figma-code-connect/build'
74
- import path from 'node:path'
75
- import { fileURLToPath } from 'node:url'
76
-
77
- await buildPlugin({
78
- cwd: path.resolve(fileURLToPath(import.meta.url), '../..'),
79
- mappingsGlob: ${JSON.stringify(mappingsGlob)},
80
- mappingTablePath: ${JSON.stringify(mappingTablePath)},
81
- outDir: ${JSON.stringify(outDir)},
82
- watch: process.argv.includes('--watch'),
83
- })
84
- `;
85
- fs.mkdirSync(path.dirname(buildScriptPath), { recursive: true });
86
- fs.writeFileSync(buildScriptPath, buildScript);
87
- steps.push({ step: 'build-script', target: buildScriptPath, skipped: false });
88
- }
89
-
90
- const pkgPath = path.join(cwd, 'package.json');
91
- if (!fs.existsSync(pkgPath)) {
92
- steps.push({ step: 'package-json-scripts', target: pkgPath, skipped: true, reason: 'package.json 不存在' });
93
- } else {
94
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
95
- pkg.scripts ??= {};
96
- const already = pkg.scripts['figma:build'] !== undefined || pkg.scripts['figma:watch'] !== undefined;
97
- if (already && !force) {
98
- steps.push({ step: 'package-json-scripts', target: pkgPath, skipped: true, reason: 'figma:build/figma:watch 已存在' });
99
- } else {
100
- pkg.scripts['figma:build'] = 'node scripts/build-figma-plugin.mjs';
101
- pkg.scripts['figma:watch'] = 'node scripts/build-figma-plugin.mjs --watch';
102
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
103
- steps.push({ step: 'package-json-scripts', target: pkgPath, skipped: false });
104
- }
105
- }
106
-
107
- // 构建产物是生成物,不该进消费方的版本控制——确保 .gitignore 里有这条,不存在就补,
108
- // 不去重复判断 force:加一行已经存在的 ignore 规则没有"覆盖"这个概念,本身就是幂等的。
109
- const gitignorePath = path.join(cwd, '.gitignore');
110
- const ignoreEntry = `/${outDir}`;
111
- const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
112
- const alreadyIgnored = existing.split('\n').some((l) => l.trim() === ignoreEntry || l.trim() === outDir);
113
- if (alreadyIgnored) {
114
- steps.push({ step: 'gitignore', target: gitignorePath, skipped: true });
115
- } else {
116
- const sep = existing === '' || existing.endsWith('\n') ? '' : '\n';
117
- fs.writeFileSync(gitignorePath, `${existing}${sep}\n# figma code connect 插件构建产物\n${ignoreEntry}\n`);
118
- steps.push({ step: 'gitignore', target: gitignorePath, skipped: false });
119
- }
120
-
121
- // 跑完这一步,产物就绪、manifest 就绪,直接能导入 Figma——不依赖消费方记得再手动跑一次
122
- // build。此时大概率还没有真实映射(figma-mappings/ 为空),产出的是零映射插件,正常。
123
- await buildPlugin({ cwd, mappingsGlob, mappingTablePath, outDir });
124
- steps.push({ step: 'build', target: path.join(cwd, outDir), skipped: false });
125
-
126
- return { steps };
127
- }
128
-
129
- if (isCliEntrypoint(import.meta.url)) {
130
- const args = process.argv.slice(2);
131
- const get = (flag, fallback) => {
132
- const i = args.indexOf(flag);
133
- return i === -1 ? fallback : args[i + 1];
134
- };
135
- const name = get('--name') ?? 'Mini Code Connect';
136
- const id = get('--id');
137
- if (!id) {
138
- console.error(
139
- '用法: node scripts/scaffold-plugin.mjs --id <manifest-id> ' +
140
- '[--name "<面板显示名>"] [--out figma-plugin-dist] [--components components] [--force]',
141
- );
142
- process.exit(1);
143
- }
144
-
145
- const { steps } = await scaffoldPlugin({
146
- cwd: process.cwd(),
147
- name,
148
- id,
149
- outDir: get('--out', 'figma-plugin-dist'),
150
- componentsDir: get('--components', 'components'),
151
- force: args.includes('--force'),
152
- });
153
-
154
- for (const s of steps) {
155
- if (s.results) {
156
- for (const r of s.results) {
157
- console.log(r.skipped ? `[scaffold-plugin] [${s.step}] 已存在,跳过:${r.target}` : `[scaffold-plugin] [${s.step}] 已写入:${r.target}`);
158
- }
159
- } else {
160
- console.log(
161
- s.skipped
162
- ? `[scaffold-plugin] [${s.step}] 已存在,跳过:${s.target}${s.reason ? `(${s.reason})` : ''}`
163
- : `[scaffold-plugin] [${s.step}] 已写入:${s.target}`,
164
- );
165
- }
166
- }
167
- console.log(
168
- '\n已经可以导入 Figma 了(Plugins → Development → Import plugin from manifest… 选 manifest.json)。' +
169
- '\n候选组件目录不是默认的 components/ 的话,编辑 figma-mapping.config.json 再改;' +
170
- '\n建好真实映射后用 `pnpm run figma:build`(或 `figma:watch`)重新打包。',
171
- );
172
- }
package/src/dev/demo.ts DELETED
@@ -1,207 +0,0 @@
1
- /**
2
- * 不开 Figma 也能验证映射:用假实例喂给一份模板。
3
- * 跑 `npm run demo`。
4
- *
5
- * 模板内联在本文件里,不挂靠任何消费方的 `figma-mappings/` 或 registry —— 这样这个演示脚本
6
- * 不受实际映射内容变动(增删组件)影响,专门用来讲解运行时机制。
7
- */
8
- import { renderToString } from '../runtime/render'
9
- import { validate } from '../main/validate'
10
- import type {
11
- AccessorCall,
12
- ComponentSchema,
13
- ErrorLike,
14
- InstanceLike,
15
- Template,
16
- TemplateResult,
17
- TextLike,
18
- } from '../runtime/types'
19
-
20
- type MockNode = {
21
- component: string
22
- props: Record<string, string | boolean>
23
- swaps?: Record<string, MockNode>
24
- }
25
-
26
- class ErrorHandle implements ErrorLike {
27
- readonly type = 'ERROR' as const
28
- constructor(readonly message: string) {}
29
- }
30
-
31
- class MockInstance implements InstanceLike {
32
- readonly type = 'INSTANCE' as const
33
- constructor(
34
- private node: MockNode,
35
- private calls: AccessorCall[],
36
- private depth = 0,
37
- private probe = false,
38
- ) {}
39
-
40
- get name(): string {
41
- return this.node.component
42
- }
43
-
44
- private log(c: Omit<AccessorCall, 'depth'>): void {
45
- this.calls.push({ ...c, depth: this.depth })
46
- }
47
-
48
- getString(prop: string): string {
49
- const v = this.node.props[prop]
50
- if (v === undefined) {
51
- this.log({ prop, method: 'getString', ok: false, note: '属性不存在' })
52
- return ''
53
- }
54
- this.log({ prop, method: 'getString', ok: true })
55
- return String(v)
56
- }
57
-
58
- getBoolean(prop: string): boolean
59
- getBoolean<T>(prop: string, mapping: { true: T; false: T }): T
60
- getBoolean(prop: string, mapping?: { true: unknown; false: unknown }): unknown {
61
- const v = this.node.props[prop]
62
- if (v === undefined) {
63
- this.log({ prop, method: 'getBoolean', ok: false, note: '属性不存在' })
64
- return mapping ? mapping.false : false
65
- }
66
- this.log({ prop, method: 'getBoolean', ok: true })
67
- const b = this.probe ? true : v === true || v === 'true'
68
- return mapping ? (b ? mapping.true : mapping.false) : b
69
- }
70
-
71
- getEnum<T>(prop: string, mapping: Record<string, T>): T | undefined {
72
- const keys = Object.keys(mapping)
73
- const v = this.node.props[prop]
74
- if (v === undefined) {
75
- this.log({ prop, method: 'getEnum', mappingKeys: keys, ok: false, note: '属性不存在' })
76
- return undefined
77
- }
78
- const raw = String(v)
79
- const hit = Object.prototype.hasOwnProperty.call(mapping, raw)
80
- this.log({
81
- prop,
82
- method: 'getEnum',
83
- mappingKeys: keys,
84
- ok: hit,
85
- note: hit ? undefined : `当前值 "${raw}" 不在字典里,返回了 undefined`,
86
- })
87
- return hit ? mapping[raw] : undefined
88
- }
89
-
90
- getInstanceSwap(prop: string): InstanceLike | ErrorLike | null {
91
- const child = this.node.swaps?.[prop]
92
- if (!child) {
93
- this.log({ prop, method: 'getInstanceSwap', ok: false, note: '找不到绑定该属性的子实例' })
94
- return new ErrorHandle(`找不到 instance swap 插槽 "${prop}"`)
95
- }
96
- this.log({ prop, method: 'getInstanceSwap', ok: true })
97
- return new MockInstance(child, this.calls, this.depth + 1, this.probe)
98
- }
99
-
100
- findInstance(layerName: string): InstanceLike | ErrorLike {
101
- return new ErrorHandle(`demo 不支持 findInstance("${layerName}")`)
102
- }
103
-
104
- findText(layerName: string): TextLike | ErrorLike {
105
- return new ErrorHandle(`demo 不支持 findText("${layerName}")`)
106
- }
107
-
108
- hasCodeConnect(): boolean {
109
- return this.node.swaps !== undefined
110
- }
111
- codeConnectId(): string | null {
112
- return null
113
- }
114
- executeTemplate(): TemplateResult | null {
115
- return {
116
- example: [{ type: 'INSTANCE', guid: 'mock', symbolId: '', name: this.node.component }],
117
- id: 'unmapped',
118
- }
119
- }
120
- }
121
-
122
- /** 独立的一份演示模板:Button ↔ <Button variant="..." size="..." /> */
123
- const buttonTemplate: Template = {
124
- id: 'demo-button',
125
- meta: { url: 'demo', source: 'demo', component: 'Button' },
126
- match: { componentName: 'Button' },
127
- render(instance) {
128
- const label = instance.getString('Label')
129
- const variant = instance.getEnum('Variant', { Primary: 'primary', Secondary: 'secondary', Ghost: 'ghost' })
130
- const size = instance.getEnum('Size', { Small: 'sm', Medium: 'md', Large: 'lg' })
131
- const disabled = instance.getBoolean('Disabled')
132
- const hasIcon = instance.getBoolean('Has Icon')
133
-
134
- const icon = hasIcon ? instance.getInstanceSwap('Icon') : null
135
- let iconCode
136
- if (icon && icon.type === 'INSTANCE') {
137
- const name = icon.name === 'Icon' ? 'arrow' : icon.name.toLowerCase()
138
- iconCode = [{ type: 'CODE' as const, code: `<Icon name="${name}" />` }]
139
- }
140
-
141
- return {
142
- example: [
143
- { type: 'CODE', code: `<Button\n variant="${variant}"\n size="${size}"` },
144
- ...(disabled ? [{ type: 'CODE' as const, code: '\n disabled' }] : []),
145
- ...(iconCode ? [{ type: 'CODE' as const, code: '\n icon={' }, ...iconCode, { type: 'CODE' as const, code: '}' }] : []),
146
- { type: 'CODE', code: `\n>\n ${label}\n</Button>` },
147
- ],
148
- imports: ['import { Button } from "primitives"'],
149
- id: 'demo-button',
150
- metadata: { nestable: true },
151
- }
152
- },
153
- }
154
-
155
- /** 手写一份等价于 get_context_for_code_connect 返回的 schema,用于自检 */
156
- const buttonSchema: ComponentSchema = {
157
- componentId: '1:2',
158
- componentKey: 'mock-button-key',
159
- componentName: 'Button',
160
- properties: [
161
- { name: 'Label', type: 'TEXT' },
162
- { name: 'Variant', type: 'VARIANT', variantOptions: ['Primary', 'Secondary', 'Ghost'] },
163
- { name: 'Size', type: 'VARIANT', variantOptions: ['Small', 'Medium', 'Large'] },
164
- { name: 'Disabled', type: 'BOOLEAN' },
165
- { name: 'Has Icon', type: 'BOOLEAN' },
166
- { name: 'Icon', type: 'INSTANCE_SWAP' },
167
- ],
168
- }
169
-
170
- const cases: MockNode[] = [
171
- {
172
- component: 'Button',
173
- props: { Label: '提交', Variant: 'Primary', Size: 'Medium', Disabled: false, 'Has Icon': false },
174
- },
175
- {
176
- component: 'Button',
177
- props: { Label: '下一步', Variant: 'Secondary', Size: 'Large', Disabled: false, 'Has Icon': true },
178
- swaps: { Icon: { component: 'Icon', props: { Name: 'Arrow' } } },
179
- },
180
- {
181
- component: 'Button',
182
- props: { Label: '不可用', Variant: 'Ghost', Size: 'Small', Disabled: true, 'Has Icon': false },
183
- },
184
- {
185
- // 故意用一个字典里没有的 variant 值,演示「穷举缺失」会被自检抓到
186
- component: 'Button',
187
- props: { Label: '危险', Variant: 'Danger', Size: 'Medium', Disabled: false, 'Has Icon': false },
188
- },
189
- ]
190
-
191
- for (const node of cases) {
192
- const calls: AccessorCall[] = []
193
- const result = buttonTemplate.render(new MockInstance(node, calls))
194
- const probeCalls: AccessorCall[] = []
195
- buttonTemplate.render(new MockInstance(node, probeCalls, 0, true))
196
- const findings = validate(buttonSchema, calls, probeCalls)
197
-
198
- console.log('─'.repeat(64))
199
- console.log('Figma 实例:', JSON.stringify(node.props))
200
- console.log('\n' + (result.imports ?? []).join('\n'))
201
- console.log(renderToString(result.example))
202
- if (findings.length) {
203
- console.log('\n自检:')
204
- for (const f of findings) console.log(` [${f.level}] ${f.text}`)
205
- }
206
- console.log()
207
- }
package/src/dev/export.ts DELETED
@@ -1,28 +0,0 @@
1
- /**
2
- * 从当前 registry(generate:registry 扫出来的 templates)生成仓库根目录的 codeconnect.json。
3
- * 这份文件是给「读代码仓库的 AI / agent」看的映射清单 —— 不需要开 Figma。
4
- * 跑 `npm run export`。这个引擎仓库自己没有真实映射数据时,产出的就是空列表——
5
- * 消费方项目要生成自己的 codeconnect.json,得在自己项目里跑同样的 export 逻辑。
6
- *
7
- * 插件面板里的「下载 codeconnect.json」是另一份:那份带着 Figma 侧的真实
8
- * componentKey(绑定过之后),可以覆盖到这里来。
9
- */
10
- import { writeFileSync } from 'node:fs'
11
- import { templates } from '../runtime/registry'
12
-
13
- const payload = {
14
- namespace: 'simplecodeconnect',
15
- generatedFrom: 'figma-mappings/*.figma.ts',
16
- label: 'React',
17
- mappings: templates.map((t) => ({
18
- templateId: t.id,
19
- componentName: t.match.componentName,
20
- componentKey: t.match.componentKey ?? null,
21
- url: t.meta.url,
22
- source: t.meta.source,
23
- component: t.meta.component,
24
- })),
25
- }
26
-
27
- writeFileSync('codeconnect.json', JSON.stringify(payload, null, 2) + '\n', 'utf8')
28
- console.log(`已写出 codeconnect.json(${payload.mappings.length} 条映射)`)