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
package/LICENSE CHANGED
@@ -1,16 +1,18 @@
1
- MIT License
1
+ Proprietary License
2
2
 
3
3
  Copyright (c) 2026 elaine.ma
4
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:
5
+ All rights reserved.
11
6
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
7
+ This software and associated documentation files (the "Software") are the
8
+ proprietary property of the copyright holder.
9
+
10
+ Permission is granted to install and use the Software via the published npm
11
+ package for your own projects, subject to the npm package terms.
12
+
13
+ You may not copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software source code, except as allowed by applicable law or
15
+ with prior written permission from the copyright holder.
14
16
 
15
17
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
18
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -19,3 +21,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
21
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
22
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
23
  SOFTWARE.
24
+
25
+ Issues and feedback: https://github.com/elainema0215/mini-figma-code-connect/issues
package/README.md CHANGED
@@ -2,17 +2,46 @@
2
2
 
3
3
  **Local Code Connect for Figma Professional** — 无 Org / Enterprise 也能做确定性的 `f(instance) → snippet`。
4
4
 
5
- 面向 Pro 档团队的轻量替代引擎:Figma 插件 + 将设计组件映射到消费方真实代码库的工作流。
5
+ > 源码未开源。npm 仅分发编译产物(`dist/`)。问题与反馈请到 [GitHub Issues](https://github.com/elainema0215/mini-figma-code-connect/issues)。
6
6
 
7
+ 面向 Pro 档团队的轻量替代引擎:Figma 插件 + 将设计组件映射到消费方真实代码库的工作流。
7
8
  官方 Code Connect 与 Builder.io Design System Intelligence 均要求 Organization / Enterprise 订阅;团队当前为 Professional,相关 MCP 绑定工具不可用。本引擎在本地实现等价的「组件绑定 + 属性映射」:映射进消费方仓库、执行在 Figma 插件,不依赖云端 publish。插件沙箱无法读写本地代码,因此映射生成在沙箱外(agent / CLI)完成,再经构建打包进插件。
8
9
 
9
- 完整背景评估、引擎仓库本地开发、系统架构与核心实现见本地 [核心实现.md](核心实现.md)(不纳入版本控制);方案对比见 [参考.md](参考.md)。
10
+ ---
11
+
12
+ ## 与官方 Code Connect 的差异
13
+
14
+ 以下差异均为刻意设计,而非未实现的功能缺口:
15
+
16
+ | | 官方 Code Connect | 本实现 |
17
+ |---|---|---|
18
+ | 订阅计划要求 | Organization / Enterprise,需 Full 或 Dev 席位 | 无要求,任意计划均可运行 |
19
+ | 模板存储位置 | 发布至 Figma 服务端(CLI `publish` 或 MCP `add_code_connect_map`) | 构建时打包进插件 |
20
+ | 模板执行方 | Figma 侧沙箱,在 Dev Mode / MCP 读取时执行 | 插件主线程,选中实例时执行 |
21
+ | 模板入口 | 模块顶层 `const instance = figma.selectedInstance` | `render(instance)` 函数参数 |
22
+ | 三行绑定注释 | 构建时解析 `// url= // source= // component=` | 保留注释供人工阅读,同时在 `meta` 字段中显式声明 |
23
+ | 组件发布状态要求 | 必须已发布 | 不要求,未发布时退回按组件名匹配 |
24
+ | SLOT 属性 | 支持 `getSlot()` | 未实现,仅支持 TEXT / BOOLEAN / VARIANT / INSTANCE_SWAP |
25
+ | 多框架支持 | 通过 `label` 枚举,单一设计组件可挂载多条并行映射 | 仅支持 React 一条 |
26
+ | 穷举校验 | 不提供,缺值静默 | 提供,`validate()` 自动对账 |
27
+ | 绑定关系判定方 | Figma 服务端存储的映射,或人工在 Code Connect UI 中手动指定 | agent 现场读取代码判断(mini-code-connect skill),或本地正则打分兜底 |
28
+
29
+ ---
30
+
31
+ ## 已知限制
32
+
33
+ - **尚未接入基于 REST API 与 Personal Access Token 的 schema 拉取能力**。该方案理论可行(通用 Figma REST API 不受 Code Connect 订阅限制),但目前唯一的数据来源仍是插件手动导出的 schema.json。
34
+ - **不提供跨消费方的数据新鲜度检查**。某个消费方代码库更新后,此前标记为 `guessed` / `unmatched` 的映射条目是否已可补全,需人工发起新一轮映射流程,引擎本身不做追踪。
35
+
36
+ > 构建时生成的 `registry.generated.ts` / `mapping-table.generated.json` 写在消费方 `outDir/.generated/`(默认 `figma-plugin-dist/.generated/`),经 esbuild 插件注入,不会改写 `node_modules` 内的包文件,多消费方可并行构建。
37
+
38
+ 消费方项目自身映射数据的具体缺口(未映射的组件、置信度较低的属性推断),记录于消费方 `figma-mapping-table.json` 的 `unmappedNotes` / `note` 字段中,不属于本文档的追踪范围。
10
39
 
11
40
  ---
12
41
 
13
42
  ## 消费方接入
14
43
 
15
- 面向将 `mini-figma-code-connect` 接入自身项目、并开始建立 Figma 组件映射的使用者。下文以 `trex-website/apps/rexy` 为例,所有命令默认在消费方子项目目录(例如 `apps/rexy`,而非 monorepo 根目录)下执行。
44
+ 面向将 `mini-figma-code-connect` 接入自身项目、并开始建立 Figma 组件映射的使用者。所有命令默认在消费方项目目录(含 `package.json` 的目录)下执行;若为 monorepo,请进入已安装该依赖的子包目录,勿在仓库根目录执行。
16
45
 
17
46
  ### 前置条件
18
47
 
@@ -36,27 +65,15 @@ pnpm add -D mini-figma-code-connect
36
65
  pnpm exec mini-code-connect-scaffold-plugin --id <manifest-id>
37
66
  ```
38
67
 
39
- `--id` 为必填参数,同一 Figma 账号内不可与已导入的其他插件重复(引擎仓库自身的 demo 插件 id 为 `simple-code-connect-demo`)。`--name` 为可选参数,默认值为 `"Mini Code Connect"`;仅当账号内同时安装了多个消费方插件、需要在 Figma 插件列表中加以区分时才需传入。
40
-
41
- 该命令具备幂等性:已存在的文件不会被覆盖,重复执行不会影响后续的手动修改;如需强制重新生成,附加 `--force` 参数。执行内容如下:
68
+ `--id` 必填,同一 Figma 账号内不可与已导入插件重复。`--name` 可选(默认 `"Mini Code Connect"`),多插件并存时用于区分。
42
69
 
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` |
70
+ 命令幂等:已有文件不覆盖;需重建时加 `--force`。会写入 skill、候选组件配置、manifest、构建脚本与 `package.json` scripts,并自动完成首次构建,之后可直接导入 Figma(步骤 4)。
52
71
 
53
- 命令执行完毕即可直接导入 Figma(见步骤 4),无需再手动构建一次。
54
-
55
- 如需单独执行其中某一步(例如仅重新生成 manifest),可使用以下更细粒度的命令——`scaffold-plugin` 内部即调用这两者:
72
+ 仅需其中一步时:
56
73
 
57
74
  ```bash
58
- pnpm exec mini-code-connect-install-skill # 仅执行 skill 安装
59
- pnpm exec mini-code-connect-scaffold-manifest --id <manifest-id> # 仅执行 manifest 生成
75
+ pnpm exec mini-code-connect-install-skill
76
+ pnpm exec mini-code-connect-scaffold-manifest --id <manifest-id>
60
77
  ```
61
78
 
62
79
  #### 3. 按需调整候选组件目录
@@ -91,118 +108,23 @@ pnpm exec mini-code-connect-scaffold-manifest --id <manifest-id> # 仅执行 m
91
108
 
92
109
  无 agent 可用时,`pnpm exec figma-mapping <schema.json>` 提供本地正则打分作为兜底方案(同样支持批量 schema 数组与 `--ai` 模式),判断准确度低于 agent 现场读取代码,但落盘规则保持一致。
93
110
 
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
- 通常由以下两种原因之一导致:
111
+ ## 常见问题
108
112
 
109
- 1. **脚本文件缺少可执行权限**。正常情况下不应出现(引擎仓库内相关脚本均为 `-rwxr-xr-x`);若出现,问题大概率在引擎侧,应向引擎仓库反馈。
110
- 2. **bin 软链未生成**。引擎包的 `package.json` 新增了 `bin` 条目,但消费方项目在此后未重新执行安装,`node_modules/.bin/` 中缺少对应命令。执行以下命令刷新:
111
- ```bash
112
- pnpm install
113
- ```
113
+ 1. **Q:** 命令找不到 / `EACCES: spawn …`
114
+ **A:** 先在已安装依赖的目录执行 `pnpm install`,刷新 `node_modules/.bin`。若仍报错,多半是引擎包脚本权限问题,反馈给引擎仓库。
114
115
 
115
- #### `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL`
116
+ 2. **Q:** `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL`
117
+ **A:** 不要在 monorepo 根目录跑 `pnpm exec`;进入已安装 `mini-figma-code-connect` 的子包再执行。
116
118
 
117
- 该错误通常表示 `pnpm exec` 是在 monorepo 根目录执行的,触发了对所有工作区包的递归遍历,其中部分包并未安装该依赖。切换至具体子项目目录(例如 `apps/rexy`)后重新执行。
119
+ 3. **Q:** 和官方 Code Connect 会冲突吗?
120
+ **A:** 不会覆盖。本方案用 `mini-code-connect` skill 与 `figma-mapping.config.json`,官方用 `figma-code-connect` / `figma.config.json`;格式不兼容,勿混用。
118
121
 
119
- #### 与项目中已有的其他 Figma Code Connect 相关 skill/config 是否存在冲突
122
+ 4. **Q:** 改了 manifest,Figma 没变化?
123
+ **A:** `editorType` / `capabilities` 在导入时缓存。需在 Figma 里删掉该开发插件后重新导入。`figma-plugin-dist/` 下的 `code.js` / `ui.html` 每次运行都会重读,改构建产物不必重导。
120
124
 
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` 字段中,不属于本文档的追踪范围。
125
+ 5. **Q:** 导入/运行插件时报 `EPERM: operation not permitted, open '…/figma-plugin-dist/code.js'`,或提示 *Unable to load code* / *loading the plugin environment*?
126
+ **A:** 这通常不是包本身坏了,而是 **macOS 隐私权限拦住了 Figma 读取本地文件**(项目在 `Desktop` / `Documents` 下时很常见)。处理:
127
+ 1. **系统设置 隐私与安全性 → 文件与文件夹**(必要时再开「完全磁盘访问权限」),给 **Figma** 勾选桌面 / 文稿等对应目录。
128
+ 2. 完全退出并重启 Figma。
129
+ 3. Development 里删掉旧插件后,重新 **Import plugin from manifest…**。
130
+ 也可把消费方仓库挪出受保护目录(例如 `~/Projects/`)再导入。
package/dist/build.mjs ADDED
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ // scripts/build-plugin.mjs
4
+ import * as esbuild from "esbuild";
5
+ import { existsSync as existsSync2 } from "node:fs";
6
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
7
+ import path4 from "node:path";
8
+
9
+ // scripts/is-cli-entrypoint.mjs
10
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ function isCliEntrypoint(importMetaUrl) {
14
+ if (!process.argv[1]) return false;
15
+ try {
16
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(importMetaUrl));
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function findPackageRoot(importMetaUrl) {
22
+ let dir = path.dirname(fileURLToPath(importMetaUrl));
23
+ while (true) {
24
+ const pkgPath = path.join(dir, "package.json");
25
+ if (existsSync(pkgPath)) {
26
+ try {
27
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
28
+ if (pkg.name === "mini-figma-code-connect") return dir;
29
+ } catch {
30
+ }
31
+ }
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir) {
34
+ throw new Error("[mini-figma-code-connect] \u627E\u4E0D\u5230\u5305\u6839\u76EE\u5F55\uFF08package.json\uFF09");
35
+ }
36
+ dir = parent;
37
+ }
38
+ }
39
+
40
+ // scripts/generate-registry.mjs
41
+ import fs from "node:fs";
42
+ import path2 from "node:path";
43
+ function parseGlob(glob) {
44
+ const starIdx = glob.indexOf("*");
45
+ if (starIdx === -1) throw new Error(`generate-registry: glob \u91CC\u6CA1\u6709 *\uFF1A"${glob}"`);
46
+ const dir = glob.slice(0, starIdx).replace(/\/$/, "");
47
+ const recursive = glob.includes("**");
48
+ return { dir, recursive };
49
+ }
50
+ function findFigmaFiles(dir, recursive) {
51
+ const results = [];
52
+ function walk(d) {
53
+ if (!fs.existsSync(d)) return;
54
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
55
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
56
+ const full = path2.join(d, entry.name);
57
+ if (entry.isDirectory()) {
58
+ if (recursive) walk(full);
59
+ } else if (entry.name.endsWith(".figma.ts")) {
60
+ results.push(full);
61
+ }
62
+ }
63
+ }
64
+ walk(dir);
65
+ return results.sort();
66
+ }
67
+ function identifierFor(filePath, index) {
68
+ const base = path2.basename(filePath).replace(/\.figma\.ts$/, "").replace(/[^a-zA-Z0-9]/g, "");
69
+ const safe = base && /^[a-zA-Z_]/.test(base) ? base : `M${base}`;
70
+ return `${safe || "Mapping"}_${index}`;
71
+ }
72
+ function generateRegistry({ cwd, mappingsGlob, outFile, typesImport = "./types" }) {
73
+ const { dir, recursive } = parseGlob(mappingsGlob);
74
+ const files = findFigmaFiles(path2.resolve(cwd, dir), recursive);
75
+ const outDir = path2.dirname(outFile);
76
+ const imports = files.map((f, i) => {
77
+ const id = identifierFor(f, i);
78
+ let rel = path2.relative(outDir, f).replace(/\.ts$/, "");
79
+ if (!rel.startsWith(".")) rel = `./${rel}`;
80
+ rel = rel.split(path2.sep).join("/");
81
+ return { id, importPath: rel };
82
+ });
83
+ const typeLine = typesImport == null ? "" : `import type { Template } from '${typesImport}'
84
+ `;
85
+ const templatesAnn = typesImport == null ? "" : ": Template[]";
86
+ 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
87
+ ` + 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(", ")}]
88
+ `;
89
+ fs.mkdirSync(outDir, { recursive: true });
90
+ fs.writeFileSync(outFile, body);
91
+ return { count: files.length, files };
92
+ }
93
+ if (isCliEntrypoint(import.meta.url)) {
94
+ const [, , mappingsGlob, outFile] = process.argv;
95
+ if (!mappingsGlob || !outFile) {
96
+ console.error("\u7528\u6CD5: node scripts/generate-registry.mjs <mappingsGlob> <outFile>");
97
+ process.exit(1);
98
+ }
99
+ const result = generateRegistry({ cwd: process.cwd(), mappingsGlob, outFile: path2.resolve(outFile) });
100
+ console.log(`[generate-registry] \u5199\u5165 ${result.count} \u6761\u6620\u5C04\u5230 ${outFile}`);
101
+ }
102
+
103
+ // scripts/copy-mapping-table.mjs
104
+ import fs2 from "node:fs";
105
+ import path3 from "node:path";
106
+ function copyMappingTable({ cwd, mappingTablePath, outFile }) {
107
+ const src = path3.resolve(cwd, mappingTablePath);
108
+ const content = fs2.existsSync(src) ? fs2.readFileSync(src, "utf8") : "[]\n";
109
+ fs2.mkdirSync(path3.dirname(outFile), { recursive: true });
110
+ fs2.writeFileSync(outFile, content);
111
+ return { found: fs2.existsSync(src) };
112
+ }
113
+
114
+ // scripts/build-plugin.mjs
115
+ var PACKAGE_ROOT = findPackageRoot(import.meta.url);
116
+ function resolvePluginEntries(packageRoot) {
117
+ const main = path4.join(packageRoot, "dist/main/code.js");
118
+ const ui = path4.join(packageRoot, "dist/ui/ui.js");
119
+ const html = path4.join(packageRoot, "dist/ui/ui.html");
120
+ for (const file of [main, ui, html]) {
121
+ if (!existsSync2(file)) {
122
+ throw new Error(
123
+ `[build-plugin] \u7F3A\u5C11 ${file}\u3002\u8BF7\u5148\u5728\u5F15\u64CE\u5305\u76EE\u5F55\u6267\u884C npm run build\uFF08\u53EA\u4F7F\u7528 dist/\uFF0C\u4E0D\u56DE\u9000 src/\uFF09\u3002`
124
+ );
125
+ }
126
+ }
127
+ return { main, ui, html };
128
+ }
129
+ function consumerGeneratedPlugin(genDir) {
130
+ const registryFile = path4.join(genDir, "registry.generated.ts");
131
+ const mappingTableFile = path4.join(genDir, "mapping-table.generated.json");
132
+ return {
133
+ name: "consumer-generated",
134
+ setup(build2) {
135
+ build2.onResolve({ filter: /(?:^|[\\/])registry\.generated(?:\.(?:ts|js))?$/ }, () => ({
136
+ path: registryFile
137
+ }));
138
+ build2.onResolve({ filter: /(?:^|[\\/])mapping-table\.generated\.json$/ }, () => ({
139
+ path: mappingTableFile
140
+ }));
141
+ }
142
+ };
143
+ }
144
+ async function buildPlugin({
145
+ cwd,
146
+ mappingsGlob,
147
+ mappingTablePath = "figma-mapping-table.json",
148
+ outDir = "dist",
149
+ watch = false
150
+ }) {
151
+ const { main: mainEntry, ui: uiEntry, html: uiHtml } = resolvePluginEntries(PACKAGE_ROOT);
152
+ const absOutDir = path4.resolve(cwd, outDir);
153
+ const genDir = path4.join(absOutDir, ".generated");
154
+ const registryOut = path4.join(genDir, "registry.generated.ts");
155
+ const mappingTableOut = path4.join(genDir, "mapping-table.generated.json");
156
+ const { count } = generateRegistry({
157
+ cwd,
158
+ mappingsGlob,
159
+ outFile: registryOut,
160
+ typesImport: null
161
+ });
162
+ console.log(`[build-plugin] \u626B\u5230 ${count} \u6761\u6620\u5C04\uFF08${mappingsGlob}\uFF09`);
163
+ const { found } = copyMappingTable({ cwd, mappingTablePath, outFile: mappingTableOut });
164
+ if (!found) console.log(`[build-plugin] \u6CA1\u627E\u5230 ${mappingTablePath}\uFF0C\u4E0B\u8F7D\u6309\u94AE\u4F1A\u7ED9\u7A7A\u6570\u7EC4`);
165
+ const generatedPlugin = consumerGeneratedPlugin(genDir);
166
+ const inlineUi = {
167
+ name: "inline-ui",
168
+ setup(build2) {
169
+ build2.onEnd(async (res) => {
170
+ if (res.errors.length) return;
171
+ const js = res.outputFiles ? res.outputFiles[0].text : await readFile(path4.join(absOutDir, ".ui.tmp.js"), "utf8");
172
+ const html = await readFile(uiHtml, "utf8");
173
+ await mkdir(absOutDir, { recursive: true });
174
+ await writeFile(path4.join(absOutDir, "ui.html"), html.replace("/*__UI_JS__*/", () => js));
175
+ console.log(`[inline-ui] ${path4.join(outDir, "ui.html")} \u5DF2\u66F4\u65B0`);
176
+ });
177
+ }
178
+ };
179
+ const codeOpts = {
180
+ entryPoints: [mainEntry],
181
+ bundle: true,
182
+ format: "iife",
183
+ target: "es2020",
184
+ outfile: path4.join(absOutDir, "code.js"),
185
+ logLevel: "info",
186
+ plugins: [generatedPlugin]
187
+ };
188
+ const uiOpts = {
189
+ entryPoints: [uiEntry],
190
+ bundle: true,
191
+ format: "iife",
192
+ target: "es2020",
193
+ outfile: path4.join(absOutDir, ".ui.tmp.js"),
194
+ logLevel: "warning",
195
+ plugins: [inlineUi, generatedPlugin]
196
+ };
197
+ if (watch) {
198
+ const a = await esbuild.context(codeOpts);
199
+ const b = await esbuild.context(uiOpts);
200
+ await Promise.all([a.watch(), b.watch()]);
201
+ console.log("[build-plugin] watching...");
202
+ return { watching: true };
203
+ }
204
+ await esbuild.build(codeOpts);
205
+ await esbuild.build(uiOpts);
206
+ return { watching: false };
207
+ }
208
+ if (isCliEntrypoint(import.meta.url)) {
209
+ const args = process.argv.slice(2);
210
+ const watch = args.includes("--watch");
211
+ const outIdx = args.indexOf("--out");
212
+ const outDir = outIdx !== -1 ? args[outIdx + 1] : "dist";
213
+ const mappingsGlob = args.find((a, i) => !a.startsWith("--") && args[i - 1] !== "--out");
214
+ if (!mappingsGlob) {
215
+ console.error("\u7528\u6CD5: node scripts/build-plugin.mjs <mappingsGlob> [--out dist] [--watch]");
216
+ process.exit(1);
217
+ }
218
+ await buildPlugin({ cwd: process.cwd(), mappingsGlob, outDir, watch });
219
+ }
220
+ export {
221
+ buildPlugin
222
+ };