mini-figma-code-connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/mini-code-connect/SKILL.md +100 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/figma-mapping.config.example.json +11 -0
- package/package.json +70 -0
- package/scripts/build-plugin.mjs +132 -0
- package/scripts/copy-mapping-table.mjs +19 -0
- package/scripts/figma-mapping/ai-generate.mjs +102 -0
- package/scripts/figma-mapping/index.mjs +250 -0
- package/scripts/figma-mapping/lib.mjs +112 -0
- package/scripts/figma-mapping/registry.mjs +12 -0
- package/scripts/figma-mapping/scaffold.mjs +153 -0
- package/scripts/generate-registry.mjs +94 -0
- package/scripts/install-skill.mjs +79 -0
- package/scripts/is-cli-entrypoint.mjs +21 -0
- package/scripts/scaffold-manifest.mjs +84 -0
- package/scripts/scaffold-plugin.mjs +172 -0
- package/src/dev/demo.ts +207 -0
- package/src/dev/export.ts +28 -0
- package/src/main/code.ts +422 -0
- package/src/main/handle.ts +207 -0
- package/src/main/messages.ts +49 -0
- package/src/main/schema.ts +45 -0
- package/src/main/validate.ts +49 -0
- package/src/runtime/define.ts +6 -0
- package/src/runtime/index.ts +21 -0
- package/src/runtime/registry.ts +14 -0
- package/src/runtime/render.ts +26 -0
- package/src/runtime/tagged.ts +46 -0
- package/src/runtime/types.ts +86 -0
- package/src/ui/ui.html +114 -0
- package/src/ui/ui.ts +271 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mini-code-connect
|
|
3
|
+
description: 把 Figma 组件 schema.json(单个或数组)映射成 (a) 消费方项目里的 .figma.ts 模板,(b) 纯数据版的 figma-mapping-table.json(供其他项目/skill 直接查表,不依赖运行时)。用户提到"映射"、"mapping"、提供 schema.json,或要求"跑一下 mini-code-connect"时使用。**跟官方 figma-code-connect skill 的区别**:不需要 Organization/Enterprise plan,读的是插件导出的 schema.json(不是实时 Figma MCP),产出格式是 defineTemplate/code,不是官方 figma.connect()/parser 格式——两者不兼容,不要混用。
|
|
4
|
+
disable-model-invocation: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Mini Code Connect
|
|
8
|
+
|
|
9
|
+
将 Figma 组件 schema 关联到真实代码组件,生成 `.figma.ts`(`defineTemplate`/`code` 格式,区别于官方 `figma.connect()` 格式,见下方“格式参考”)。
|
|
10
|
+
|
|
11
|
+
**这个 skill 本身(`mini-figma-code-connect` 引擎)不装真实映射数据**——它是被各个消费方项目(比如 `trex-website/apps/rexy`)以 npm 依赖(或本地 `link:`)装进去用的一个纯引擎包。真正的 `.figma.ts`、`figma-mapping-table.json`、`figma-mapping.config.json` 都应该生成在**消费方项目自己的目录里**,不是这个引擎仓库里。执行前先确认当前要处理的是哪个消费方项目(通常从用户的上下文或已打开的项目判断;如果不确定,问一句)。
|
|
12
|
+
|
|
13
|
+
这是 agent 驱动的工作流,不是脚本——由 agent 直接阅读代码判断组件对应关系。`scripts/figma-mapping/` 下有一套本地正则打分 CLI 作为无 agent 场景的兜底方案,运行时以**当前工作目录**(即消费方项目根目录,而不是这个引擎仓库)为准来读 config、写 `.figma.ts`。
|
|
14
|
+
|
|
15
|
+
## 输入
|
|
16
|
+
|
|
17
|
+
单个 `ComponentSchema`,或其数组(插件"导出全部 schema.json"的产出)。数组按顺序逐个处理:完成一个(写入文件)再处理下一个;遇到需要用户确认的项先跳过,继续处理数组中其余可确定的项,待遇到的问题集中在最后一并询问,避免整批阻塞。
|
|
18
|
+
|
|
19
|
+
## 步骤
|
|
20
|
+
|
|
21
|
+
1. **查重。** 检查消费方项目 `figma-mappings/*.figma.ts` 中是否已存在同名或同 key 的 `match`(`registry.generated.ts` 是自动生成的,不体现实际列表,查重要看 `.figma.ts` 文件本身)。已存在时询问用户保留、覆盖还是作为新映射处理——同一批 schema 可能混合了已处理和未处理的项,不查重会导致重复生成。
|
|
22
|
+
|
|
23
|
+
2. **定位候选组件。** 读取消费方项目根目录下 `figma-mapping.config.json` 的 `codeConnect.paths`(不存在就复制引擎包的 [figma-mapping.config.example.json](../../../figma-mapping.config.example.json) 到消费方项目根目录创建一份——内容都是项目内相对路径,可以直接提交 git)。**注意文件名是 `figma-mapping.config.json` 不是 `figma.config.json`**——后者是官方 Figma Code Connect CLI 认的文件名,消费方项目里可能已经有一份官方的(比如 `trex-website/apps/rexy` 就有一份官方 `figma.config.json` + 顶层 `Button.figma.ts`,那是完全独立的另一套东西,不要混淆也不要覆盖)。拿到 `paths` 后遍历这些目录下的 `.ts`/`.tsx` 文件(排除 `.figma.ts`),阅读 Props 定义判断语义是否对应——组件名相似度仅供参考,Props 语义一致性才是判定依据。没有合适候选时明确说明,不做牵强匹配。
|
|
24
|
+
|
|
25
|
+
3. **映射属性。** 按 [handle.ts](../../../src/main/handle.ts) 的 accessor 对应关系处理:
|
|
26
|
+
|
|
27
|
+
| Figma 类型 | accessor |
|
|
28
|
+
|---|---|
|
|
29
|
+
| TEXT | `getString('属性名')` |
|
|
30
|
+
| BOOLEAN | `getBoolean('属性名')`——若代码侧是其他形式(例如从某个枚举拆分出的 disabled/loading),按实际语义处理 |
|
|
31
|
+
| VARIANT | `getEnum('属性名', {...})`,字典须覆盖 `variantOptions` 的每一个值,遗漏会静默返回 undefined |
|
|
32
|
+
| INSTANCE_SWAP | `getInstanceSwap('属性名')` + `.executeTemplate()?.example`,使用前需判断 `.type === 'INSTANCE'` |
|
|
33
|
+
|
|
34
|
+
无法确定的判断仍需给出结论,但要在文件头部注释中注明"此处为推测"。确实无法推断的情况(缺少精确对应的图标组件、无法确定文案图层名)应停下询问用户,不得生成语法正确但语义错误的内容。禁止编造代码中不存在的 prop。
|
|
35
|
+
|
|
36
|
+
4. **生成 `.figma.ts`**,格式见下方“格式参考”。写到消费方项目根目录的 `figma-mappings/<组件名>.figma.ts`。`meta.source` 字段填写真实组件文件相对消费方项目根目录的路径。`imports` 按 `figma-mapping.config.json` 的 `importPaths` 做前缀匹配换算。
|
|
37
|
+
|
|
38
|
+
5. **同步写入一条 `figma-mapping-table.json` 记录。** 该文件面向本仓库之外的消费方(其他 skill、其他项目查表使用),与 `.figma.ts` 内容对应但格式不同——纯数据,不含 `defineTemplate`/`getEnum` 等仅本仓库运行时可识别的内容。默认文件名为 `figma-mapping-table.json`(与 `buildPlugin` / `scaffold-plugin` 默认一致)。记录结构:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"figmaComponentName": "...",
|
|
43
|
+
"figmaComponentKey": "...",
|
|
44
|
+
"codeComponent": "...",
|
|
45
|
+
"codeSource": "相对候选组件所在仓库根目录的路径",
|
|
46
|
+
"codeImportSpecifier": "真实 import 使用的 specifier",
|
|
47
|
+
"props": [
|
|
48
|
+
{ "figmaProp": "...", "figmaType": "...", "codeProp": "..." 或 null,
|
|
49
|
+
"valueMap": {...} 或 null, "confidence": "matched" | "guessed" | "unmatched",
|
|
50
|
+
"note": "判断依据,guessed/unmatched 必须填写" }
|
|
51
|
+
],
|
|
52
|
+
"unmappedNotes": ["属性完全无法推断时记录在此,例如缺少 TEXT 属性导致拿不到文案图层名"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`codeSource`/`codeImportSpecifier` 需与该记录来源的候选目录对应(消费方项目里 `figma-mapping.config.json` 声明的候选目录);`props` 逐条对应第 3 步的判断结果,`confidence`/`note` 为必填字段。**此步骤需保持幂等**:按 `figmaComponentKey`(缺失时按 `figmaComponentName`)检查 `figma-mapping-table.json` 中是否已有同一条记录,存在则整条替换,不存在则追加,不产生重复记录。
|
|
57
|
+
|
|
58
|
+
6. **落盘前向用户展示结果**,标注所有推测性判断,待确认后再写入。
|
|
59
|
+
|
|
60
|
+
7. **写入范围限定在当前消费方项目**:`figma-mappings/*.figma.ts`、`figma-mapping-table.json`。**绝不写入这个引擎仓库(`mini-figma-code-connect`)自己**——它不装任何真实映射数据,只提供 `defineTemplate`/`code` 运行时和 CLI/构建脚本。如果当前上下文涉及多个候选消费方项目、不确定该往哪个项目写,先问用户。
|
|
61
|
+
|
|
62
|
+
8. **全部处理完成后统一验证一次**,而非逐个组件执行——在**消费方项目**里跑它自己的类型检查 + 插件构建脚本(例如 `trex-website/apps/rexy` 是):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm --filter rexy run type-check
|
|
66
|
+
pnpm --filter rexy run figma:build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`figma:build` 背后调用的是引擎包导出的 `buildPlugin()`,会自动先重新扫描消费方项目的 `figma-mappings/**/*.figma.ts` 生成 `registry.generated.ts`——新建/修改的 `.figma.ts` 会自动被收录,不需要手动加 import。出现编译错误须就地修复。通过后提示用户回到 Figma 插件面板,通过"重新读取"或"扫描页面"确认效果。
|
|
70
|
+
|
|
71
|
+
## 格式参考
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { defineTemplate, code } from 'mini-figma-code-connect'
|
|
75
|
+
|
|
76
|
+
export default defineTemplate({
|
|
77
|
+
meta: {
|
|
78
|
+
url: 'https://www.figma.com/design/FILE_KEY/...?node-id=NODE_ID',
|
|
79
|
+
source: '相对消费方项目根目录的真实组件文件路径',
|
|
80
|
+
component: 'Button',
|
|
81
|
+
},
|
|
82
|
+
id: 'button',
|
|
83
|
+
match: { componentName: 'Button', componentKey: 'FIGMA_COMPONENT_KEY' },
|
|
84
|
+
|
|
85
|
+
render(instance) {
|
|
86
|
+
const label = instance.getString('Label')
|
|
87
|
+
const disabled = instance.getBoolean('Disabled')
|
|
88
|
+
const size = instance.getEnum('Size', { Large: 'large', Medium: 'medium', Small: 'small' })
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
example: code`<Button size="${size}"${disabled ? code` disabled` : ''}>${label}</Button>`,
|
|
92
|
+
imports: ['import { Button } from "@/components/button"'],
|
|
93
|
+
id: 'button',
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- INSTANCE_SWAP 嵌套写法、无属性组件、推测性判断标注等更复杂的实际案例,参考已经生成在消费方项目里的 `.figma.ts` 文件本身(比如 `trex-website/apps/rexy/figma-mappings/`),不再维护在这个引擎仓库里。
|
|
100
|
+
- [types.ts](../../../src/runtime/types.ts) 的 `InstanceLike`——accessor 类型定义(`getString`/`getBoolean`/`getEnum`/`getInstanceSwap`/`findText`)。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 elaine.ma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# mini-figma-code-connect
|
|
2
|
+
|
|
3
|
+
**Local Code Connect for Figma Professional** — 无 Org / Enterprise 也能做确定性的 `f(instance) → snippet`。
|
|
4
|
+
|
|
5
|
+
面向 Pro 档团队的轻量替代引擎:Figma 插件 + 将设计组件映射到消费方真实代码库的工作流。
|
|
6
|
+
|
|
7
|
+
官方 Code Connect 与 Builder.io Design System Intelligence 均要求 Organization / Enterprise 订阅;团队当前为 Professional,相关 MCP 绑定工具不可用。本引擎在本地实现等价的「组件绑定 + 属性映射」:映射进消费方仓库、执行在 Figma 插件,不依赖云端 publish。插件沙箱无法读写本地代码,因此映射生成在沙箱外(agent / CLI)完成,再经构建打包进插件。
|
|
8
|
+
|
|
9
|
+
完整背景评估、引擎仓库本地开发、系统架构与核心实现见本地 [核心实现.md](核心实现.md)(不纳入版本控制);方案对比见 [参考.md](参考.md)。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 消费方接入
|
|
14
|
+
|
|
15
|
+
面向将 `mini-figma-code-connect` 接入自身项目、并开始建立 Figma 组件映射的使用者。下文以 `trex-website/apps/rexy` 为例,所有命令默认在消费方子项目目录(例如 `apps/rexy`,而非 monorepo 根目录)下执行。
|
|
16
|
+
|
|
17
|
+
### 前置条件
|
|
18
|
+
|
|
19
|
+
- 消费方项目为 npm/pnpm 项目(存在 `package.json`),且位于 git 仓库内——脚手架命令依赖 `.git` 定位仓库根目录。
|
|
20
|
+
- Node.js ≥ 18。
|
|
21
|
+
|
|
22
|
+
### 接入步骤
|
|
23
|
+
|
|
24
|
+
#### 1. 安装依赖
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add -D mini-figma-code-connect
|
|
28
|
+
# 或:npm install -D mini-figma-code-connect
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
本地开发引擎时仍可用 `link:` / `file:` 指向仓库路径。包尚未安装时,后续 CLI 命令均不存在。
|
|
32
|
+
|
|
33
|
+
#### 2. 执行一次性脚手架
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm exec mini-code-connect-scaffold-plugin --id <manifest-id>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`--id` 为必填参数,同一 Figma 账号内不可与已导入的其他插件重复(引擎仓库自身的 demo 插件 id 为 `simple-code-connect-demo`)。`--name` 为可选参数,默认值为 `"Mini Code Connect"`;仅当账号内同时安装了多个消费方插件、需要在 Figma 插件列表中加以区分时才需传入。
|
|
40
|
+
|
|
41
|
+
该命令具备幂等性:已存在的文件不会被覆盖,重复执行不会影响后续的手动修改;如需强制重新生成,附加 `--force` 参数。执行内容如下:
|
|
42
|
+
|
|
43
|
+
| 步骤 | 目标位置 | 说明 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| 安装 skill | `<仓库根>/.claude/skills/mini-code-connect/`(若 `<仓库根>/.agents/skills/` 存在,同时写入一份) | 自动从当前目录向上查找 `.git` 所在的仓库根——pnpm monorepo 中子包目录并非仓库根,写入错误位置会导致 Claude Code 无法发现该 skill |
|
|
46
|
+
| 候选组件目录配置 | `<cwd>/figma-mapping.config.json` | 默认假设组件位于 `<cwd>/components`,可通过 `--components <dir>` 调整 |
|
|
47
|
+
| Manifest | `<cwd>/manifest.json` 与 `manifest.codegen.json` | `main`/`ui` 字段指向 `--out` 参数指定的目录(默认 `figma-plugin-dist`) |
|
|
48
|
+
| 构建脚本 | `<cwd>/scripts/build-figma-plugin.mjs` | 调用引擎包导出的 `buildPlugin()` |
|
|
49
|
+
| package.json | 新增 `scripts.figma:build` 与 `figma:watch` | 已存在的同名 script 不会被覆盖 |
|
|
50
|
+
| `.gitignore` | 新增构建产物目录的忽略规则 | 已存在同一条规则则跳过 |
|
|
51
|
+
| 首次构建 | `<cwd>/<outDir>/`(默认 `figma-plugin-dist/`) | 命令结束时自动执行一次构建,产出可直接导入 Figma 的插件——无需额外手动执行 `pnpm run figma:build` |
|
|
52
|
+
|
|
53
|
+
命令执行完毕即可直接导入 Figma(见步骤 4),无需再手动构建一次。
|
|
54
|
+
|
|
55
|
+
如需单独执行其中某一步(例如仅重新生成 manifest),可使用以下更细粒度的命令——`scaffold-plugin` 内部即调用这两者:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm exec mini-code-connect-install-skill # 仅执行 skill 安装
|
|
59
|
+
pnpm exec mini-code-connect-scaffold-manifest --id <manifest-id> # 仅执行 manifest 生成
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### 3. 按需调整候选组件目录
|
|
63
|
+
|
|
64
|
+
若项目的组件目录并非 `components/`,或 import 写法并非 `@/` 别名,打开步骤 2 生成的 `figma-mapping.config.json` 修改 `paths`/`importPaths`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"codeConnect": {
|
|
69
|
+
"paths": { "components": "components" },
|
|
70
|
+
"importPaths": { "components/*": "@/components/*" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
文件内容均为项目内相对路径,可直接提交至 git。修改该文件不影响已生成的插件产物,不需要重新构建——它仅在建立映射时(步骤 5)由 skill/CLI 读取,用于定位候选组件。
|
|
76
|
+
|
|
77
|
+
#### 4. 导入 Figma
|
|
78
|
+
|
|
79
|
+
**Figma 桌面版 → 菜单 → Plugins → Development → Import plugin from manifest…**,选择 `<cwd>/manifest.json`。
|
|
80
|
+
|
|
81
|
+
如需 Dev Mode Code 区形态,以相同方式导入 `manifest.codegen.json`(`id` 不同,为独立记录,二者可同时导入)。
|
|
82
|
+
|
|
83
|
+
此时插件已可运行,但尚无真实映射数据,所有实例均显示为「未映射」。
|
|
84
|
+
|
|
85
|
+
#### 5. 建立真实映射
|
|
86
|
+
|
|
87
|
+
1. 在插件面板中选中未映射实例,点击「导出 schema.json」(候选列表页可使用「导出全部 schema.json」批量导出)。
|
|
88
|
+
2. 在该消费方项目的 Claude Code 会话中(而非引擎仓库的会话),提供 schema.json 路径,或直接描述映射需求,以触发 `/mini-code-connect` skill。该 skill 将执行查重、扫描步骤 3 配置的候选目录、生成 `figma-mappings/*.figma.ts` 与 `figma-mapping-table.json`,写入位置为当前消费方项目,不写入引擎仓库。
|
|
89
|
+
3. 执行 `pnpm run figma:build`,将新生成的映射重新打包进插件。
|
|
90
|
+
4. 返回插件面板,点击「重新读取」或「扫描页面」,确认状态变为「已映射」。
|
|
91
|
+
|
|
92
|
+
无 agent 可用时,`pnpm exec figma-mapping <schema.json>` 提供本地正则打分作为兜底方案(同样支持批量 schema 数组与 `--ai` 模式),判断准确度低于 agent 现场读取代码,但落盘规则保持一致。
|
|
93
|
+
|
|
94
|
+
### 命令参考
|
|
95
|
+
|
|
96
|
+
| 命令 | 必填参数 | 作用 |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `mini-code-connect-scaffold-plugin` | `--id` | 执行完整一次性脚手架(详见步骤 2) |
|
|
99
|
+
| `mini-code-connect-install-skill` | 无 | 仅将 skill 复制至项目。`--root <path>` 显式指定仓库根,`--no-claude-mirror` 仅写入 `.agents/skills/`,`--force` 覆盖已存在内容 |
|
|
100
|
+
| `mini-code-connect-scaffold-manifest` | `--id` | 仅生成 manifest.json 与 manifest.codegen.json。`--out <dir>` 指定产物目录 |
|
|
101
|
+
| `figma-mapping` | `<schema.json>` | 本地正则打分 CLI,agent 缺席时的兜底方案。`--ai` 调用本机 `claude` CLI 以无头模式执行判断 |
|
|
102
|
+
|
|
103
|
+
### 常见问题
|
|
104
|
+
|
|
105
|
+
#### `EACCES: spawn <command>`
|
|
106
|
+
|
|
107
|
+
通常由以下两种原因之一导致:
|
|
108
|
+
|
|
109
|
+
1. **脚本文件缺少可执行权限**。正常情况下不应出现(引擎仓库内相关脚本均为 `-rwxr-xr-x`);若出现,问题大概率在引擎侧,应向引擎仓库反馈。
|
|
110
|
+
2. **bin 软链未生成**。引擎包的 `package.json` 新增了 `bin` 条目,但消费方项目在此后未重新执行安装,`node_modules/.bin/` 中缺少对应命令。执行以下命令刷新:
|
|
111
|
+
```bash
|
|
112
|
+
pnpm install
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL`
|
|
116
|
+
|
|
117
|
+
该错误通常表示 `pnpm exec` 是在 monorepo 根目录执行的,触发了对所有工作区包的递归遍历,其中部分包并未安装该依赖。切换至具体子项目目录(例如 `apps/rexy`)后重新执行。
|
|
118
|
+
|
|
119
|
+
#### 与项目中已有的其他 Figma Code Connect 相关 skill/config 是否存在冲突
|
|
120
|
+
|
|
121
|
+
`mini-code-connect` 这一 skill 名称与 `figma-mapping.config.json` 这一配置文件名,均刻意与官方 Figma Code Connect(`figma-code-connect` skill、`figma.config.json`、`.figma.ts` + `figma.connect()`/`figma.code` 官方格式)区分。两者格式不兼容,不应混用,也不应将本手册的步骤应用于官方格式的文件。若项目中两者并存,`.claude/skills/`(及 `.agents/skills/`,如适用)下应可见 `mini-code-connect` 与 `figma-code-connect` 两个独立目录,互不覆盖。
|
|
122
|
+
|
|
123
|
+
#### 修改 manifest 后 Figma 无变化
|
|
124
|
+
|
|
125
|
+
`editorType`/`capabilities` 由 Figma 在插件注册时读取一次并缓存,修改 manifest 文件本身不会自动生效,必须在 Figma 中删除该开发插件并重新导入。构建产物(`figma-plugin-dist/` 下的 `code.js`/`ui.html`)则会在插件每次运行时重新读取,无需删除重导。
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 项目结构
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
manifest.json Figma 插件清单(面板形态)——本仓库自身 demo 用,产物零映射
|
|
133
|
+
manifest.codegen.json Figma 插件清单(Dev Mode Code 区形态)——同上
|
|
134
|
+
figma-mapping.config.example.json 消费方接入时复制到自身项目根目录的配置模板
|
|
135
|
+
esbuild.config.mjs 本仓库以 buildPlugin() 自举打包自身(零映射,用于验证引擎可用性)
|
|
136
|
+
|
|
137
|
+
.claude/skills/mini-code-connect/
|
|
138
|
+
└── SKILL.md 工作流的完整步骤定义,agent 依此执行——写入目标是消费方项目,而非本仓库
|
|
139
|
+
|
|
140
|
+
scripts/figma-mapping/ 插件沙箱之外的工作流实现(CLI 以调用时的 cwd 作为 ROOT,即消费方项目根目录)
|
|
141
|
+
├── index.mjs CLI 入口:本地正则打分选取候选 + 交互确认,agent 缺席场景下的兜底方案
|
|
142
|
+
├── lib.mjs 读取消费方项目的 figma-mapping.config.json,扫描候选目录,提取组件信息并打分
|
|
143
|
+
├── scaffold.mjs 根据打分选中的候选生成 .figma.ts 内容
|
|
144
|
+
├── registry.mjs 重新生成 registry.generated.ts(薄封装,核心逻辑位于 scripts/generate-registry.mjs)
|
|
145
|
+
└── ai-generate.mjs --ai 模式:调用本机 claude CLI 进行判断(存在 OAuth 阻塞风险,不推荐;agent 在场时应直接使用 skill)
|
|
146
|
+
|
|
147
|
+
scripts/
|
|
148
|
+
├── build-plugin.mjs 导出 buildPlugin({cwd, mappingsGlob, mappingTablePath, outDir, watch})
|
|
149
|
+
├── generate-registry.mjs 导出 generateRegistry({cwd, mappingsGlob, outFile}),扫描 .figma.ts 生成 registry.generated.ts
|
|
150
|
+
├── copy-mapping-table.mjs 将消费方的 figma-mapping-table.json 复制至包自身 src/main/ 下,供 code.ts 静态引入
|
|
151
|
+
├── install-skill.mjs bin: mini-code-connect-install-skill
|
|
152
|
+
├── scaffold-manifest.mjs bin: mini-code-connect-scaffold-manifest
|
|
153
|
+
├── scaffold-plugin.mjs bin: mini-code-connect-scaffold-plugin —— 单条命令完成上述两项 + 候选目录配置 + 构建脚本 + package.json scripts
|
|
154
|
+
└── is-cli-entrypoint.mjs 内部工具:判定"是否被直接作为 CLI 执行"(process.argv[1] 与 import.meta.url 的朴素比较通常不成立,详见文件内注释)
|
|
155
|
+
|
|
156
|
+
src/
|
|
157
|
+
├── runtime/ 轻量级 Code Connect 运行时(与 Figma API 无关,可独立测试)——包的公开入口
|
|
158
|
+
│ ├── index.ts 导出 defineTemplate/code/类型定义,消费方通过 `import { defineTemplate, code } from 'mini-figma-code-connect'` 引入
|
|
159
|
+
│ ├── types.ts ResultSection / ComponentSchema / Template / InstanceLike
|
|
160
|
+
│ ├── tagged.ts figma.code`...` 的简化实现:切分 section,展平嵌套结构
|
|
161
|
+
│ ├── render.ts ResultSection[] 转字符串
|
|
162
|
+
│ ├── define.ts defineTemplate(),用于类型收窄
|
|
163
|
+
│ ├── registry.generated.ts 自动生成(已加入 .gitignore),由 scripts/generate-registry.mjs 扫描消费方 .figma.ts 产出;本地 build/typecheck 前生成
|
|
164
|
+
│ └── registry.ts componentKey / componentName 到模板的查找;templates 数组从 registry.generated 转出
|
|
165
|
+
├── main/ 插件主线程(可访问 figma.* API)
|
|
166
|
+
│ ├── code.ts 六步流水线与面板消息处理,详见核心实现.md
|
|
167
|
+
│ ├── schema.ts 组件到属性 schema 的提取
|
|
168
|
+
│ ├── handle.ts InstanceHandle:访问器层与递归执行
|
|
169
|
+
│ ├── validate.ts Step 6 自检的自动化实现
|
|
170
|
+
│ ├── mapping-table.generated.json 自动生成(已加入 .gitignore),构建时从消费方的 figma-mapping-table.json 复制
|
|
171
|
+
│ └── messages.ts 主线程与 UI 间的消息类型定义
|
|
172
|
+
├── ui/ 插件面板
|
|
173
|
+
│ ├── ui.html
|
|
174
|
+
│ └── ui.ts
|
|
175
|
+
└── dev/
|
|
176
|
+
├── demo.ts 独立示例模板,使用模拟实例渲染,不依赖 Figma 或任何消费方数据
|
|
177
|
+
└── export.ts 生成 codeconnect.json
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 与官方 Code Connect 的差异
|
|
183
|
+
|
|
184
|
+
以下差异均为刻意设计,而非未实现的功能缺口:
|
|
185
|
+
|
|
186
|
+
| | 官方 Code Connect | 本实现 |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| 订阅计划要求 | Organization / Enterprise,需 Full 或 Dev 席位 | 无要求,任意计划均可运行 |
|
|
189
|
+
| 模板存储位置 | 发布至 Figma 服务端(CLI `publish` 或 MCP `add_code_connect_map`) | 构建时打包进插件 |
|
|
190
|
+
| 模板执行方 | Figma 侧沙箱,在 Dev Mode / MCP 读取时执行 | 插件主线程,选中实例时执行 |
|
|
191
|
+
| 模板入口 | 模块顶层 `const instance = figma.selectedInstance` | `render(instance)` 函数参数 |
|
|
192
|
+
| 三行绑定注释 | 构建时解析 `// url= // source= // component=` | 保留注释供人工阅读,同时在 `meta` 字段中显式声明 |
|
|
193
|
+
| 组件发布状态要求 | 必须已发布 | 不要求,未发布时退回按组件名匹配 |
|
|
194
|
+
| SLOT 属性 | 支持 `getSlot()` | 未实现,仅支持 TEXT / BOOLEAN / VARIANT / INSTANCE_SWAP |
|
|
195
|
+
| 多框架支持 | 通过 `label` 枚举,单一设计组件可挂载多条并行映射 | 仅支持 React 一条 |
|
|
196
|
+
| 穷举校验 | 不提供,缺值静默 | 提供,`validate()` 自动对账 |
|
|
197
|
+
| 绑定关系判定方 | Figma 服务端存储的映射,或人工在 Code Connect UI 中手动指定 | agent 现场读取代码判断(mini-code-connect skill),或本地正则打分兜底 |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 已知限制
|
|
202
|
+
|
|
203
|
+
- **尚未接入基于 REST API 与 Personal Access Token 的 schema 拉取能力**。该方案理论可行(通用 Figma REST API 不受 Code Connect 订阅限制),但目前唯一的数据来源仍是插件手动导出的 schema.json。
|
|
204
|
+
- **不提供跨消费方的数据新鲜度检查**。某个消费方代码库更新后,此前标记为 `guessed` / `unmatched` 的映射条目是否已可补全,需人工发起新一轮映射流程,引擎本身不做追踪。
|
|
205
|
+
|
|
206
|
+
> 构建时生成的 `registry.generated.ts` / `mapping-table.generated.json` 写在消费方 `outDir/.generated/`(默认 `figma-plugin-dist/.generated/`),经 esbuild 插件注入,不会改写 `node_modules` 内的包文件,多消费方可并行构建。
|
|
207
|
+
|
|
208
|
+
消费方项目自身映射数据的具体缺口(未映射的组件、置信度较低的属性推断),记录于消费方 `figma-mapping-table.json` 的 `unmappedNotes` / `note` 字段中,不属于本文档的追踪范围。
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mini-figma-code-connect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Local Code Connect for Figma Professional — 无 Org 也能做确定性的 f(instance) → snippet。",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "elaine.ma <elaine.ma@aspendigital.co>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/elainema0215/mini-figma-code-connect.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/elainema0215/mini-figma-code-connect/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/elainema0215/mini-figma-code-connect#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"figma",
|
|
18
|
+
"code-connect",
|
|
19
|
+
"figma-plugin",
|
|
20
|
+
"design-to-code",
|
|
21
|
+
"figma-mappings"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src/**/*.ts",
|
|
28
|
+
"src/**/*.html",
|
|
29
|
+
"scripts",
|
|
30
|
+
".claude/skills/mini-code-connect",
|
|
31
|
+
"figma-mapping.config.example.json",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"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"
|
|
42
|
+
},
|
|
43
|
+
"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"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node esbuild.config.mjs",
|
|
52
|
+
"watch": "node esbuild.config.mjs --watch",
|
|
53
|
+
"pretypecheck": "npm run generate:registry && npm run generate:mapping-table",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"generate:registry": "node scripts/generate-registry.mjs 'figma-mappings/**/*.figma.ts' src/runtime/registry.generated.ts",
|
|
56
|
+
"generate:mapping-table": "node -e \"import('./scripts/copy-mapping-table.mjs').then(m => m.copyMappingTable({ cwd: process.cwd(), mappingTablePath: 'figma-mapping-table.json', outFile: 'src/main/mapping-table.generated.json' }))\"",
|
|
57
|
+
"demo": "esbuild src/dev/demo.ts --bundle --platform=node --format=esm --outfile=dist/demo.mjs --log-level=warning && node dist/demo.mjs",
|
|
58
|
+
"preexport": "npm run generate:registry",
|
|
59
|
+
"export": "esbuild src/dev/export.ts --bundle --platform=node --format=esm --outfile=dist/export.mjs --log-level=warning && node dist/export.mjs",
|
|
60
|
+
"prepublishOnly": "npm run build"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"esbuild": "^0.25.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@figma/plugin-typings": "^1.109.0",
|
|
67
|
+
"typescript": "^5.6.3",
|
|
68
|
+
"@types/node": "^22.10.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|