关于 framer-plugins
一款使用 Framer Plugin SDK 构建、调试和修改 Framer 插件的开发参考技能,涵盖 SDK 设置、插件模式与 API,并提供检查及选择性更新社区维护说明的指南。
主要功能
- 01SDK 设置与迁移指南
- 02插件模式和 API 参考
- 03CMS 集合工作流
- 04版本更新检查
- 05可选的上游贡献
适用场景
- 01搭建新的 Framer 插件
- 02将插件迁移到 SDK v4
- 03调试画布或 CMS 插件行为
- 04检查技能是否有更新
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ fredm00n/ framerlabs --skill framer-plugins安装后在 Claude Code 中输入/framer-plugins确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-reference.mdcms-managed-collections.mdmarketplace.mdpatterns.mdpitfalls.md详细介绍
Framer 插件开发指南
你是 Framer Plugin SDK 方面的专家。构建、调试或修改 Framer 插件时,请参考此指南。务必检查项目的 CLAUDE.md,了解项目特定的覆盖规则。
更新与贡献
此技能由公开仓库 fredm00n/framerlabs 维护,社区会持续更新。已安装的副本会逐渐过时,因此以下步骤会在每个会话开始时运行一次。
项目约定
项目的 Agent 指令(CLAUDE.md、AGENTS.md 或同类文件)可以为此技能添加一行:
framer-plugins: auto-update, contribute
auto-update 允许 Agent 在发现有更新版本时替换已安装的副本。contribute 允许 Agent 提议将会话中的经验反馈到仓库,并以拉取请求的形式提交。单独使用任一词语均可。如果没有这一行,Agent 绝不会修改已安装的文件,也不会创建或建议提交拉取请求;它只会告知你有新版本。
先检查是否有新版本
在使用以下内容之前:
- 获取
https://raw.githubusercontent.com/fredm00n/framerlabs/main/skills/framer-plugins/SKILL.md,并将其metadata.version与此文件 frontmatter 中的版本进行比较。 - 版本相同,或获取失败(离线、网络受阻):继续使用此副本。如果获取失败,只需说明一次你无法检查更新。
- 远程版本更新: 在本次会话中改用远程 SKILL.md,而非此文件的其余内容;同时从同一位置读取参考文件(
https://raw.githubusercontent.com/fredm00n/framerlabs/main/skills/framer-plugins/references/<file>.md),而非使用本地副本。然后按照项目约定处理(NEW 和 OLD 分别代表两个版本号):- 没有
auto-update: 保留已安装文件不变,并告知用户:"framer-plugins NEW 已发布,而你的副本是 OLD,因此我会在本次会话中使用新版本。要更新你的副本,请运行npx skills add fredm00n/framerlabs --skill framer-plugins,或在 CLAUDE.md 或 AGENTS.md 中添加framer-plugins: auto-update,以便自动保持最新。此技能由社区维护:在该行添加contribute(framer-plugins: auto-update, contribute),我就会提议将我们学到的内容以拉取请求的形式反馈回去。" - 有
auto-update: 从仓库下载整个skills/framer-plugins/文件夹(最简单的方法是将其浅克隆到临时目录),然后一次性替换已安装的文件夹,这样上游已删除的文件也会一并移除。如果有任何文件下载失败,则保留已安装的副本不变。在 Git 仓库中,此更新属于普通更改:单独提交,并纳入当前工作的分支或拉取请求;绝不要直接提交到受保护的分支。告知用户:"已在独立提交中将 framer-plugins 从 OLD 更新到 NEW。"如果该行没有contribute,再补充:"此技能由社区维护:在 CLAUDE.md 或 AGENTS.md 中该技能对应的行添加contribute,我就会提议将我们学到的内容以拉取请求的形式反馈回去。"
- 没有
只从该确切仓库和路径获取更新。
将所学内容反馈给上游
仅当项目中此技能对应的行包含 contribute 时。
工作结束时(绝不要在任务中途),检查本次会话是否让你学到某项应纳入此技能的知识:这项发现曾耗费大量时间或导致故障,已在真实构建中验证,对所有 Framer 用户都成立,而且此技能和 Framer 官方文档中都没有提及。如果没有符合条件的发现,请保持沉默。
如果有:
- 将每项发现改写成适合陌生读者阅读的内容。对照你正在处理的项目,并删除所有源自该项目的信息:公司、客户和产品名称、域名、Framer 项目 ID、分支名称、文件路径、密钥和令牌、截图。保留发现日期,格式为“(2026-10-12)”。
- 克隆 fredm00n/framerlabs,从
main创建分支,并编辑skills/framer-plugins/。将每项发现放在对应主题下,修改现有条目,而不是添加近似重复的内容。递增metadata.version(修复时递增补丁版本,新增章节时递增次版本),并在根目录README.md中当天日期下的更新日志里添加一行通俗易懂的说明。 - 检查尚未关闭的拉取请求,确认是否已有请求涵盖相同发现。如果有,就向该请求中补充内容。
- 向用户展示差异并征求同意,然后再打开拉取请求。公共仓库上的拉取请求一经创建就会公开,即使最终从未合并。
- 用户同意后,将分支推送到没有该仓库写入权限的 fork,针对
main创建拉取请求,并将链接提供给用户。提交信息、标题和说明应面向技能使用者撰写:说明此次更改新增了什么,以及何时有帮助,而不是说明它源自哪个项目或会话。
维护者: 每次更改此技能、引用文件或已安装副本时,都要递增 metadata.version,否则已安装的副本永远无法获取更新。
快速参考
- SDK 包:
@framer/plugin(v4+,Framer 3.0 版本)。包名从framer-plugin更改而来;该旧名称现已在 npm 上弃用,请勿使用。 - 脚手架:
npm create framer-plugin@latest(create-framer-plugin脚手架名称不变,并会安装@framer/plugin) - 构建:Vite +
vite-plugin-framer(名称不变;framer-plugin-toolspackCLI 也不变) - 基础样式:
import "@framer/plugin/framer.css"(可选:import "@framer/plugin/inter.css"可单独加载 Inter) - 核心导入:
import { framer } from "@framer/plugin" - 开发工作流:
npm run dev→ Framer → Developer Tools → Development Plugin
SDK v4(2026-06-16)和近期 v3.x 新增内容
v4.0 本身主要是重命名(版本归属已根据 2026-07-13 的官方更新日志核实)。v4 中发布的内容:软件包重命名 framer-plugin → @framer/plugin(当前稳定版为 4.0.1;旧软件包已在 npm 上弃用;迁移只需机械地查找并替换——没有移除常用 API;peerDeps react/react-dom ^18.2.0、csstype ^3.1.1)、重新设计样式的 framer.css(Framer 3.0 设计:新增 Inter 字符变体、选中颜色、输入元素样式——升级后请重新检查自定义输入/选中样式;新增 @framer/plugin/inter.css 导出,仅加载 Inter)、WebPageNode.clone() / DesignPageNode.clone()、getLocalizationGroups() 上可选的筛选器、新的 frame/text 链接属性,以及接受 UnknownNode 的 setParent。
v3.x 期间新增(2025 年至 2026 年初)——比许多已发布示例更新,均不受权限门控:
framer.navigateTo(nodeId, opts?)(3.7.0)——选中节点并缩放至视图中(opts: { select?=true, zoom? })。实例形式:node.navigateTo()、collectionItem.navigateTo()、codeFile.navigateTo()。非常适合在插入/替换后立即调用。framer.setMenu(items)和framer.showContextMenu(items, config)(3.5.2)——全局窗口标题栏菜单,以及插件 UI 任意位置的上下文菜单。framer.subscribeToOpenCodeFile(cb)(3.7.0)——用户打开/切换代码文件时触发。- Design Pages(3.8.0):通过相同的 Nodes + Selection API,在 Design Pages 上读取/创建/编辑节点。
- CMS:访问所有 Collections(不只是由插件管理的集合),并写入非托管集合(Plugins 3.0,2025 年 3 月);
framer.createManagedCollection()(3.10.2);framer.setCloseWarning()(3.10.3)。framer.getManagedCollection()已弃用 → 请改用getActiveManagedCollection()(弃用信息在 d.ts 中;CMS 文档页面的示例仍展示旧调用)。
创建新插件
npm create framer-plugin@latest→ 脚手架会生成 Vite + React +framer.json+ 示例App。- 先选择模式(见下表)。CMS 插件需要
configureManagedCollection+syncManagedCollection;资源/插入插件使用canvas(或image/editImage)。 import "@framer/plugin/framer.css",并在useLayoutEffect中调用framer.showUI(...)。- 尽早决定存储方式:每位用户的密钥使用
localStorage,共享状态使用pluginData(参见下方决策树)。
要克隆现有插件来创建新插件? 将
framer.json中的id改为新的十六进制值。重复的id会让 Framer 将两个插件视为同一个插件——它们会共享安装状态、localStorage源和pluginData命名空间,导致开发数据在不知情的情况下相互污染。脚手架会生成唯一的id;复制代码仓库时请保留此属性。
framer.json
每个插件都需要在项目根目录中有一个 framer.json:
{
"id": "6bbb4f",
"name": "My Plugin",
"modes": ["configureManagedCollection", "syncManagedCollection"],
"icon": "/icon.svg"
}
id— 唯一的十六进制标识符(由脚手架自动生成)modes— 支持的模式数组(见下文)icon— 30×30,SVG 或位图(位图请提供 90×90 px 文件),放在public/中。SVG 需要仔细调整居中位置。
插件模式
| 模式 | 用途 | framer.mode 值 |
|---|---|---|
canvas | 通用画布访问 | "canvas" |
configureManagedCollection | CMS:首次设置/字段配置 | "configureManagedCollection" |
syncManagedCollection | CMS:重新同步现有集合 | "syncManagedCollection" |
image | 用户选择图像 | "image" |
editImage | 编辑现有图像 | "editImage" |
collection | 访问用户可编辑的集合 | "collection" |
code | 编辑当前打开的代码文件 | "code" |
localization | 编辑当前语言区域 | "localization" |
CMS 插件会同时使用 configureManagedCollection + syncManagedCollection。(v4 d.ts 的 Mode 联合类型还包含未记录的 api 值——除非文档开始提及,否则请忽略。)
核心 framer API
UI 管理
framer.showUI({ position?, width, height, minWidth?, minHeight?, maxWidth?, resizable? })
framer.hideUI()
framer.closePlugin(message?, { variant: "success" | "error" | "info" }) // returns never
framer.notify(message, { variant?, durationMs?, button?: { text, onClick } })
framer.setCloseWarning(message | false) // warn before closing during sync
framer.setBackgroundMessage(message) // status while plugin runs hidden
framer.setMenu([{ label, onAction, visible? }, { type: "separator" }])
framer.showContextMenu(items, { location }) // context menu inside the plugin UI (3.0)
framer.navigateTo(nodeId, { select?, zoom? }) // select + zoom a node into view (3.0; not gated)
framer.subscribeToOpenCodeFile(callback) // fires on code-file open/switch (3.0)
closePlugin在内部会抛出FramerPluginClosedError——请始终在 catch 块中忽略- 应在
useLayoutEffect中调用showUI,以避免闪烁
属性
framer.mode— 当前模式字符串
集合访问
framer.getActiveManagedCollection() // → Promise<ManagedCollection>
framer.getActiveCollection() // → Promise<Collection> (unmanaged)
framer.getManagedCollections() // → Promise<ManagedCollection[]>
framer.getCollections() // → Promise<Collection[]>
framer.createManagedCollection() // → Promise<ManagedCollection>
画布方法(画布模式)
framer.addImage(image) // NamedImageAssetInput | File → Promise<void>
framer.setImage(image) // sets on the current selection
framer.getImage()
framer.addText(text, options?)
framer.addSVG(svg) // SVGData; max 10kB
framer.addComponentInstance({ url, attributes?, parentId? }) // → Promise<ComponentInstanceNode>
framer.addDetachedComponentLayers({ url, layout, attributes? }) // → Promise<FrameNode> (inlines the layers)
framer.getSelection()
framer.subscribeToSelection(callback)
addComponentInstance 会解析为创建的节点——请保留它;在需要根据选择进行替换或复制几何信息的流程中,你需要它的 id。addImage/addSVG/addText 会解析为 void。
CMS / ManagedCollection(指针)
CMS 插件有其独立的领域——完整的接口(ManagedCollection API、字段类型、静默同步算法、用户可编辑字段、slug 生成、字段数据类型包装器、CMS 常见问题)位于 cms-managed-collections.md。如果插件要将外部数据源同步到 Framer 集合,请阅读该文档;否则可跳过。
即使不打开该文件,也请记住以下两点:
ManagedCollection.addItems()是一个更新或插入操作(按id匹配)——而且没有getItems(),只有getItemIds()。- 每个
fieldData值都需要显式类型包装器:{ type: "string", value: "..." }。原始值会被静默忽略。
权限
import { framer, useIsAllowedTo, type ProtectedMethod } from "@framer/plugin"
// Imperative check
framer.isAllowedTo("ManagedCollection.addItems", "ManagedCollection.removeItems")
// React hook (reactive)
const canSync = useIsAllowedTo("ManagedCollection.addItems", "ManagedCollection.removeItems")
// Standard CMS sync permissions
const SYNC_METHODS = [
"ManagedCollection.setFields",
"ManagedCollection.addItems",
"ManagedCollection.removeItems",
"ManagedCollection.setPluginData",
] as const satisfies ProtectedMethod[]
**只有会修改数据的方法受权限控制。**读取方法——getItemIds、getPluginData、getActiveManagedCollection、getFields——在 SDK 的 ProtectedMethod 表中对应的数组都是空的,因此对它们调用 isAllowedTo() 始终会返回 true。对写操作进行权限控制;为提高稳健性,请用 try/catch 包装读取操作,而不是添加无效的读取权限检查。(文档本身的说法是:未受保护的方法“通常以 get 开头,但并非全部如此”——不确定时交给 TypeScript 判断:如果某个名称不被接受为 ProtectedMethod,就无需对其进行权限检查。)
**Marketplace 审核约定(已通过 verdict 测试,2026-07):**自动审核要求在每个受保护的调用位置同时满足以下三点:在调用附近显式检查 framer.isAllowedTo()(仅在上游较早位置进行响应式 useIsAllowedTo 提前返回,本身不算);用 try/catch 包装调用,并提供清晰的面向用户的失败提示(不得静默忽略);缺少权限时,通过 useIsAllowedTo 禁用 UI。死代码或暂存代码中未进行权限检查的受保护调用也会被标记——请删除。详情:marketplace.md。
数据存储决策树
| 需求 | 使用 | 原因 |
|---|---|---|
| API 密钥、身份验证令牌 | localStorage | 按用户隔离,无大小警告,不会共享 |
| 用户偏好设置 | localStorage | 按用户隔离、同步 |
| 数据源 ID、上次同步时间 | collection.setPluginData() | 在协作者之间共享,与集合关联 |
| 项目级配置 | framer.setPluginData() | 可共享,但总上限为 4kB |
| 大型/持续增长的状态(逐项同步映射、缓存) | localStorage | 映射增长时,pluginData 的每条目 2kB 上限会很快耗尽;localStorage 可容纳约数 MB |
| 许可证/权益状态 | 你的后端,以 framer.getCurrentUser().id 为键 | 用户 ID(64 个字符的十六进制值)在不同项目/设备间对同一账户保持稳定;客户端无需存放任何可能泄露、共享或被带入项目 Remix 的内容。对于付费插件,这是最稳妥的选择——参见 pitfalls.md |
pluginData:每条目 2kB,总计 4kB。仅支持字符串。传入null即可删除。pluginData适用于小型、大小有上限的值。 任何会随内容数量增长的内容(逐项增量同步状态、缓存)都会超出每条目 2kB 的上限——应将其存放在localStorage中,并将其视为可重建的优化项,而非共享的事实来源(该数据按用户隔离,因此协作者首次使用时会重建)。pluginData按插件命名空间隔离——其他插件(以及外部 Agent 工具)无法读取或写入你的键。localStorage:按插件来源进行沙盒隔离,可在所有项目间持久保存,但仅限于每个浏览器/设备——这只能带来轻微阻碍,绝不能作为强制措施。- 调用
setPluginData()(及其他受保护方法)前未先调用framer.isAllowedTo()检查时,会显示“Invoking protected message type”提示。这是权限系统的行为,并非 bug——在调用前进行权限检查即可消除该提示。
已授权插件与项目 Remix
项目 Remix 时会原样复制 pluginData——许可证密钥和凭据会带入新项目,并且看起来仍然有效。检测 Remix,并在加载时清除过期凭据:
const { id: currentProjectId } = await framer.getProjectInfo()
if (storedProjectId !== currentProjectId) {
// Remixed → wipe credentials so the new owner does a fresh activation
if (framer.isAllowedTo("setPluginData")) {
await framer.setPluginData(PD_LICENSE_KEY, null)
// ... clear any other project-bound keys
}
setLicenseActive(false)
}
激活时存储项目 ID,并在每次加载时进行比较;若 isAllowedTo 为 false,则静默跳过清除(下次加载时会重试)。完整模式和提供商说明见 pitfalls.md。
代码文件与托管模块(画布模式)
已在生产环境的画布插件中针对 @framer/plugin v4 验证(2026-06):
framer.createCodeFile(name, code, { editViaPlugin?: boolean })
// editViaPlugin: true → the file's "Edit Code" action opens this plugin instead of the editor
framer.getCodeFiles() // → Promise<CodeFile[]>
codeFile.setFileContent(code) // new version; UPDATES already-inserted canvas instances in place
codeFile.typecheck() // TS diagnostics without leaving the plugin
codeFile.exports // [{ name, componentId, insertURL, type }]
framer.addComponentInstance({ url }) // accepts insertURL or any module URL
instance.setAttributes({ controls }) // rewrites property-control values in place, repeatable
- 通过
componentIdentifier匹配画布实例,绝不要使用insertURL。 该标识符在重新生成后保持稳定:local-module:codeFile/{fileId}:{exportName}。insertURL会固定到某个版本(…/Name-hash.js@{versionId}),并在每次setFileContent时改变。 - 代码文件可以相互使用相对路径(
./Engine.tsx)导入,也可以通过绝对模块 URL导入 — 构建流水线会保持 URL 导入有效。 - 每个代码文件都会公开托管在
https://framer.com/m/{Name}-{hash}.js[@version]— 无需身份验证,即使是私有项目也一样。未固定版本的 URL 会立即指向最新保存版本(无需发布步骤);固定版本的 URL 不可变,即使文件被删除也仍然有效。 addComponentInstance({ url })可以使用其他项目的模块 URL:标识符会变为module:{moduleId}/{version}/{Name}.js:{export}(按module:{moduleId}/前缀匹配),使用方项目中不会出现代码文件,而源文件更新会在实例上显示为手动“更新”按钮。@framerDisableUnlink(组件上方的注释标注)会阻止对共享/托管组件执行“编辑代码”或取消链接。它无法隐藏本地代码文件 — 这些文件始终会在“资源”>“代码”中显示并可编辑。- 代码文件导入(已于 2026-07 验证): 只有
react、react-dom、framer、framer-motion支持裸 npm 说明符;带版本后缀的说明符(react@18)会被拒绝。其他依赖都必须通过 URL 导入 — 请固定版本(例如https://esm.sh/<pkg>@<version>?external=react,react-dom,将 react 设为外部依赖,以便 Framer 运行时提供自身版本)。URL 会原样保留在编译后的模块中,因此固定版本的 insertURL 会永久固定其依赖树。静默失败特征: 代码文件的exports始终为[],但类型检查通过,这表示存在不受支持的导入 — 构建会失败,但不会显示错误。 - 跨项目插入时,不要依赖
@framerIntrinsic的宽度/高度(已于 2026-07 验证): 即使编译后的元数据完全相同,addComponentInstance({ url })也只会对部分托管模块采用该标注。如果插入尺寸很重要,请在插入后显式设置:node.setAttributes({ width: "600px", height: "400px" })。
"@framer/plugin" 的关键导出
import { framer, useIsAllowedTo, FramerPluginClosedError } from "@framer/plugin"
import type {
ManagedCollection, ManagedCollectionField, ManagedCollectionFieldInput,
ManagedCollectionItemInput, FieldDataInput, FieldDataEntryInput,
ProtectedMethod, Collection, CollectionItem
} from "@framer/plugin"
import "@framer/plugin/framer.css"
参考资料
如需深入了解,请参阅此技能目录中的配套文件:
- api-reference.md — 完整的 API 签名、节点几何信息、权限方法、类型定义
- cms-managed-collections.md — CMS 插件:ManagedCollection API、同步算法、字段类型、slug、CMS 易错点(非 CMS 插件可跳过)
- patterns.md — 跨领域模式:身份验证(OAuth / API 密钥)、UI 尺寸、进度、弹性、菜单、画布插入
- pitfalls.md — 已知注意事项、项目 remix / 许可模式、调试技巧
- marketplace.md — 提交流程、列表规范、审核流程、政策、发布后的义务
关键规则
始终
- 检查项目的
CLAUDE.md,了解项目特定的覆盖规则和决策。 - 开发任何新功能之前,先查看 marketplace.md。当前官方文档称,插件发布后会立即上线,无需审核;并将质量标准(英文 UI、浅色和深色模式、以 USD 计价、不含广告、遵守 IP 规则)列为建议——但根据实测,自动化的发布后代码扫描会在数小时内强制检查安全性和权限相关要求,并可能下架已上线的条目(判定测试于 2026-07)。请将这些建议视为硬性要求来开发。
- 导入
"@framer/plugin/framer.css"以使用原生样式。 - 使用
<div role="button">,而不是<button>,以避免 Framer 的 CSS 覆盖样式。 - 在 catch 代码块中处理
FramerPluginClosedError——静默忽略。 - 在
useLayoutEffect中调用showUI,以避免调整大小时闪烁。 - 每次执行受保护的(会修改数据的)调用前,都要紧邻检查
framer.isAllowedTo()(同步执行,返回boolean);用 try/catch 包装调用,并向用户展示失败提示;同时通过useIsAllowedTo禁用触发该调用的 UI——市场审核人员会检查每个调用点是否满足这三项要求。读取操作不受限制,因此不要为其添加无实际作用的保护检查。将原始源文件和 sourcemaps 一并打包(参见 marketplace.md)。 - 使用
localStorage存储每位用户的密钥,使用pluginData存储共享状态——项目 remix 不匹配时,清除凭据(参见上文“已授权插件”)。
画布 / 代码文件插件
9. 使用 componentIdentifier 匹配画布实例,绝不要使用 insertURL(其版本固定,每次保存文件都会变化)。
10. 生成或管理的代码文件应传入 { editViaPlugin: true },使“编辑代码”操作将用户带回插件。
11. 不要在 UI 中渲染标题栏——Framer 的窗口边框已显示插件名称和关闭按钮;添加 1px 的 border-top 即可分隔内容。
12. 克隆插件仓库时,为 framer.json 设置新的 id(重复的 id 会共享状态)。
CMS 插件——详情请参阅 cms-managed-collections.md:
13. 显示 UI 之前,先在 syncManagedCollection 模式下尝试静默同步。
14. addItems() 是 upsert;fieldData 的值必须显式指定 { type, value };绝不要先移除所有条目再重新添加;绝不要在 fieldData 中包含 userEditable 字段。