Novu 开源仓库实战:use_figma 的 Figma 插件 API 避坑指南 —— 常见错误与正确写法全解析
在 Novu 开源仓库的 .agents/skills 中维护着一套面向 Agent 的 use_figma 技能,它允许通过纯 JavaScript 脚本在 Figma 文件中执行创建、编辑、变量绑定与设计系统构建等操作。本文以技能参考文档 gotchas.md 为骨架,系统梳理了 use_figma 场景下最常踩中的 30 余个陷阱,并为每一条提供 WRONG(错误)与 CORRECT(正确)双份可运行代码示例。读完本文,你将掌握颜色取值、Auto Layout、组件/变体、Variable 绑定、页面切换、节点尺寸与布局等方向的正确调用姿势,能有效规避“脚本报错”“静默失效”“布局错乱”三大类高频故障,并在出错后依据错误信息快速自愈。
本文主体对应技能参考文档
.agents/skills/figma-use/references/gotchas.md;技能总则见 SKILL.md,类型签名以 plugin-api-standalone.d.ts 为准(该文件为约 1.1 万行的类型声明,可按符号名 grep 定位)。
目录(按主题归类)
- 画布创建与节点定位:新建节点默认落在 (0,0)、reparenting 后坐标不重置、网格混排重叠、Sections 不自动缩放
- 组件属性与变体:
addComponentProperty返回字符串 key、combineAsVariants的两个前提、变体网格需手动布局 - Paint 与颜色:0–1 取值域、数组不可变、paint
color不含a、空 fills 无法绑定变量 - 页面上下文与脚本生命周期:
currentPage同步赋值不可用、脚本必须return、禁止figma.notify()、getPluginData不可用 - Auto Layout 与尺寸顺序:FILL 需先入父容器、HUG 父容器压扁 FILL 子节点、
layoutGrow压缩、resize()重置 sizing mode、width/height 只读 - 字体与排版:
lineHeight/letterSpacing必须是对象、字体样式名需通过listAvailableFontsAsync发现 - Variable 作用域与模式:集合自带 1 个 mode、作用域默认
ALL_SCOPES、显式 mode 需逐组件设置、模式数量受套餐限制、CSS 变量名不含空格 - 类型安全与属性写入:类型守卫前置、对象不可扩展错误、
detachInstance()使祖先 ID 失效
一、新建节点默认落在 (0,0) —— 顶层节点必须手动避让
figma.create*() 创建出的节点坐标默认为 (0,0)。如果直接把多个节点 append 到页面,它们会互相重叠,并压住页面已有内容。
需要注意:这条规则只对直接 append 到页面的顶层节点生效。作为 Frame、Component 或 Auto Layout 容器子节点加入的节点,其位置由父容器负责,嵌套场景无需做重叠扫描。
// WRONG —— 顶层节点落在 (0,0),与已有页面内容重叠
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)
// CORRECT —— 先扫描已有内容的边界,把新顶层节点放到最右侧
const page = figma.currentPage
let maxX = 0
for (const child of page.children) {
const right = child.x + child.width
if (right > maxX) maxX = right
}
const frame = figma.createFrame()
frame.name = "My New Frame"
frame.resize(400, 300)
figma.currentPage.appendChild(frame)
frame.x = maxX + 100 // 与最右侧内容保持 100px 间距
frame.y = 0
// NOT NEEDED —— 容器内的子节点无需重叠扫描
const card = figma.createAutoLayout('VERTICAL')
const label = figma.createText()
card.appendChild(label) // 由 auto-layout 定位,无需设置 x/y
use_figma 技能规则第 13 条也明确要求“将新顶层节点放在 (0,0) 之外”,并指向本文的避让示例。
二、addComponentProperty 返回的是字符串 key —— 严禁硬编码或猜测
Figma 会动态生成组件属性的 key(例如 "label#4:0"),后缀不可预测。必须捕获返回值并直接使用。
// WRONG —— 猜测 / 硬编码 key
comp.addComponentProperty('label', 'TEXT', 'Button')
labelNode.componentPropertyReferences = { characters: 'label#0:1' } // Error: key not found
// WRONG —— 把返回值当成对象处理
const result = comp.addComponentProperty('Label', 'TEXT', 'Button')
const propKey = Object.keys(result)[0] // BUG: 返回 '0'(字符串的首个字符下标!)
labelNode.componentPropertyReferences = { characters: propKey } // Error: property '0' not found
// CORRECT —— 返回值本身就是 key 字符串,直接使用
const propKey = comp.addComponentProperty('Label', 'TEXT', 'Button')
// propKey === "label#4:0"(具体值随文件变化;永远不要假设它)
labelNode.componentPropertyReferences = { characters: propKey }
在类型声明 plugin-api-standalone.d.ts 中,addComponentProperty 的签名正是 (propertyName, type, defaultValue, options?): string,可返回带唯一后缀的属性名;COMPONENT_SET 节点同理,返回值同样是字符串 key。
三、所有创建/变更的节点 ID 必须随返回值返回
凡是创建或改动画布节点的脚本,都必须在返回值中记录并返回所有受影响的节点 ID。否则后续调用无法引用、校验或清理这些节点——这正是技能规则第 15 条“MUST return ALL created/mutated node IDs”的来源。
// WRONG —— 只返回父 Frame 的 ID,丢失子节点
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
return { nodeId: frame.id }
// CORRECT —— 以结构化响应返回全部新建节点 ID
const frame = figma.createFrame()
const rect = figma.createRectangle()
const text = figma.createText()
frame.appendChild(rect)
frame.appendChild(text)
return {
createdNodeIds: [frame.id, rect.id, text.id],
rootNodeId: frame.id
}
// CORRECT —— 变更既有节点时同样返回这些 ID
const nodes = figma.currentPage.findAll(n => n.name === 'Card')
for (const n of nodes) {
n.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
}
return {
mutatedNodeIds: nodes.map(n => n.id),
count: nodes.length
}
四、Paint 与颜色相关陷阱
1. 颜色取值域是 0–1,不是 0–255
Figma 的 RGB 通道一律使用 0–1 浮点范围。写成 255 会被 ZeroToOne 校验拦截并抛错。
// WRONG —— 抛出 validation error
node.fills = [{ type: 'SOLID', color: { r: 255, g: 0, b: 0 } }]
// CORRECT
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
RGB 与 RGBA 类型声明均注明 0–1 range, not 0–255(见 plugin-api-standalone.d.ts 中 RGB/RGBA 类型注释)。若数据源是 0–255,记得先除以 255。
2. fills/strokes 是不可变数组
对数组元素就地修改不会生效,Figma 不感知原地变更。必须克隆 → 修改 → 重新赋值。
// WRONG —— 原地修改无效
node.fills[0].color = { r: 1, g: 0, b: 0 }
// CORRECT —— 克隆、修改、重新赋值
const fills = JSON.parse(JSON.stringify(node.fills))
fills[0].color = { r: 1, g: 0, b: 0 }
node.fills = fills
类型索引文档 plugin-api-standalone.index.md 中同样以 CRITICAL 标注了“fills/strokes 是只读数组,需要克隆-修改-重赋”这一约束。
3. paint 的 color 里不能带 a —— 透明度属于 paint 层级的 opacity
paint 的 color 只接受 {r, g, b}。附加 a 会抛出 "Unrecognized key(s) in object: 'a' at [0].color"——这是从 CSS rgba() 写法的肌肉记忆带来的高频错误。Alpha/透明度应放在 paint 层级的 opacity 字段上。
// WRONG —— color 内部不能有 'a',会抛 validation error
node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.1 } }]
// CORRECT —— opacity 放在 paint 层级
node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 0.1 }]
// CORRECT —— 完全不透明(无需 opacity)
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
COLOR 变量值是唯一的例外——变量值确实使用 {r, g, b, a} 四通道:
// 变量值使用 {r, g, b, a} —— 仅变量场景下是正确的
const colorVar = figma.variables.createVariable("bg", collection, "COLOR")
colorVar.setValueForMode(modeId, { r: 1, g: 0, b: 0, a: 1 }) // 不透明红色
colorVar.setValueForMode(modeId, { r: 0, g: 0, b: 0, a: 0 }) // 完全透明
4. setBoundVariableForPaint 返回的是新 paint,必须接住返回值
该函数不是就地修改,而是返回一份已绑定变量的新 paint。忽略返回值继续使用旧 paint,绑定会静默丢失。
// WRONG —— 忽略返回值
figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [paint] // paint 根本没变!
// CORRECT —— 捕获返回的新 paint
const boundPaint = figma.variables.setBoundVariableForPaint(paint, "color", colorVar)
node.fills = [boundPaint]
类型声明 plugin-api-standalone.d.ts 明确注释“@returns a copy of the paint which is now bound to the provided variable”,传入 null 变量则可解除绑定。setBoundVariableForEffect、setBoundVariableForLayoutGrid 遵循同样模式。
5. paint 字段的变量绑定只对 SOLID paint 生效
setBoundVariableForPaint 只能把颜色变量绑定到 SOLID 类型的 paint 上;渐变、图片等 paint 会抛错。
// 仅 SOLID paint 支持颜色变量绑定
// 渐变 / 图片等 paint 会抛错
const solidPaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
const bound = figma.variables.setBoundVariableForPaint(solidPaint, "color", colorVar)
6. 空 fills 的节点无法绑定颜色变量
对没有 fills 的节点(fills = [],透明)执行颜色绑定没有任何意义,也不会有任何效果。
// WRONG —— 绑定到没有 fills 的节点,什么都不会发生
const comp = figma.createComponent()
comp.fills = [] // transparent
// 无法把颜色变量绑定到不存在的 fills 上
// CORRECT —— 先放一个占位 SOLID paint,再绑定变量
const comp = figma.createComponent()
const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }
const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", colorVar)
comp.fills = [boundPaint]
// 变量解析出的实际值(可能本身是透明的)将决定最终颜色
五、Variable 集合与作用域陷阱
1. 新建的 variable collection 自带 1 个 mode,先重命名而非新增
createVariableCollection 返回的集合已经内置一个名为 "Mode 1" 的 mode。正确做法是先把默认 mode 改名,再按需 addMode。
// 新集合已自带一个 mode
const collection = figma.variables.createVariableCollection("Colors")
// collection.modes = [{ modeId: "...", name: "Mode 1" }]
collection.renameMode(collection.modes[0].modeId, "Light")
const darkModeId = collection.addMode("Dark")
2. mode 命名要有语义 —— 永远不要保留 'Mode 1'
每个新建 VariableCollection 都从名为 'Mode 1' 的单个 mode 开始,请立即重命名:单 mode 集合用 'Default';多 mode 集合使用来自设计源文件的命名(如 'Light'/'Dark'、'Desktop'/'Tablet'/'Mobile')。
// WRONG —— 通用名没有任何语义
const coll = figma.variables.createVariableCollection('Colors')
// coll.modes[0].name === 'Mode 1' —— 保持原样
const darkId = coll.addMode('Mode 2')
// CORRECT —— 立即按源命名重命名
const coll = figma.variables.createVariableCollection('Colors')
coll.renameMode(coll.modes[0].modeId, 'Light') // 原为 'Mode 1'
const darkId = coll.addMode('Dark')
// 单 mode 集合(primitives、spacing 等)
const spacing = figma.variables.createVariableCollection('Spacing')
spacing.renameMode(spacing.modes[0].modeId, 'Default') // 原为 'Mode 1'
3. Variable 默认 ALL_SCOPES —— 必须显式限定 scopes
新建变量若不设置 scopes,默认值是 ["ALL_SCOPES"],会让该变量出现在所有属性选择器(fills、text、strokes、spacing 等)中,污染整个选择面板——几乎从来不是你想要的结果。
// WRONG —— 变量出现在每一个属性 picker 中
const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR")
// bgColor.scopes 默认是 ["ALL_SCOPES"] —— 污染所有下拉框
// CORRECT —— 限制到相关的属性 picker
const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR")
bgColor.scopes = ["FRAME_FILL", "SHAPE_FILL"] // 仅填充 picker
const textColor = figma.variables.createVariable("Text/Default", coll, "COLOR")
textColor.scopes = ["TEXT_FILL"] // 仅文本颜色 picker
const borderColor = figma.variables.createVariable("Border/Default", coll, "COLOR")
borderColor.scopes = ["STROKE_COLOR"] // 仅描边 picker
const spacing = figma.variables.createVariable("Space/400", coll, "FLOAT")
spacing.scopes = ["GAP"] // 仅间距/gap picker
// 隐藏仅被别名引用的底层原始 token
const primitive = figma.variables.createVariable("Brand/500", coll, "COLOR")
primitive.scopes = [] // 从所有 picker 隐藏
4. 显式 variable mode 必须按组件逐个设置
多 mode 集合中,若不为组件显式指定 mode,所有变体都会渲染默认(第一个)mode 的值,导致“换肤”不生效。
// WRONG —— 所有变体渲染默认(第一个)mode
const colorCollection = figma.variables.createVariableCollection("Colors")
// ... 创建 variables 和 modes ...
// 默认情况下所有组件都显示第一个 mode 的值!
// CORRECT —— 在每个组件上显式设置 mode,以获得变体专属的值
component.setExplicitVariableModeForCollection(colorCollection, targetModeId)
类型声明支持以集合对象或 ID 字符串两种形式调用 setExplicitVariableModeForCollection,建议直接传入集合对象(见 plugin-api-standalone.d.ts)。
5. 每个 collection 的 mode 数量受套餐限制
Figma 按团队/组织套餐限制每个集合可创建的 mode 数,越界会静默失败或抛错:
- Free:仅 1 个 mode(无法
addMode) - Professional:最多 4 个 mode
- Organization / Enterprise:可达 40+ 个 mode
// WRONG —— 在 Professional 套餐上创建 20 个 mode 会失败
const coll = figma.variables.createVariableCollection("Variants")
for (let i = 0; i < 20; i++) coll.addMode("mode" + i) // May fail!
// CORRECT —— 需要大量 mode 时拆分到多个 collection
// 例如不要用 1 个 collection 装 20 个 mode(variant × color):
// Collection A:4 个 mode(variant:plain/outlined/soft/solid)
// Collection B:5 个 mode(color:neutral/primary/danger/success/warning)
// 然后在每个组件上对两个 collection 都调用 setExplicitVariableModeForCollection
6. 由 Figma 变量名推导 CSS 变量名时,空格必须一并处理
从 Figma 变量名构造 var(--name) 时,需要把斜杠和空格都替换为连字符,并转小写。只替换斜杠会留下空格,产出 'var(--color-bg-brand secondary hover)' 这类非法 CSS。
// WRONG —— 只替换斜杠,空格残留
v.setVariableCodeSyntax('WEB', `var(--${figmaName.replace(/\//g, '-').toLowerCase()})`)
// CORRECT —— 一次同时替换所有空白与斜杠
v.setVariableCodeSyntax('WEB', `var(--${figmaName.replace(/[\s\/]+/g, '-').toLowerCase()})`)
最佳实践:优先保留来源 token 文件中已有的 CSS 变量名,而不是从 Figma 名现推:
// 推荐 —— 直接使用来源 CSS 名
v.setVariableCodeSyntax('WEB', `var(${token.cssVar})`) // 例如 '--color-bg-brand-secondary-hover'
六、页面上下文与脚本生命周期陷阱
1. 同步页面 setter 不可用 —— 必须 await figma.setCurrentPageAsync(page)
在 use_figma 中,同步 setter figma.currentPage = page 不生效,会抛 "Setting figma.currentPage is not supported"。必须改用 await figma.setCurrentPageAsync(page),它负责切换页面并加载页面内容。
注意:读取 figma.currentPage 是合法的,只有赋值会抛错。
// WRONG —— 抛 "Setting figma.currentPage is not supported"
figma.currentPage = targetPage
// CORRECT —— async 方法切换并加载内容
await figma.setCurrentPageAsync(targetPage)
// ALSO CORRECT —— 读取 currentPage 没有问题
const page = figma.currentPage // works
更进一步,每次 use_figma 调用之间页面上下文会重置:figma.currentPage 每次都从第一页开始。如果多步工作流面向非默认页,必须在每次调用开头重新 setCurrentPageAsync。技能规则第 8 条还提醒:不要漏掉 await,未 await 的异步调用会 fire-and-forget,造成静默失败或竞态。
2. get_metadata 只作用于单个子树 —— 页面需要显式枚举
一个 Figma 文件可能包含多个页面(canvas 节点)。get_metadata 只返回你传入节点的子树。要拿到所有页面的可用索引:
- 不传 nodeId 调用
get_metadata:它返回文档顶层页面作为{guid, name}条目(不含 XML dump),这是发现页面的最廉价方式。 - 需要每页更细的信息(如子节点数量、顶层节点类型)时,再回退到
use_figma:
const pages = figma.root.children.map(p => `${p.name} id=${p.id} children=${p.children.length}`);
return pages.join('\n');
图标、变量、组件可能存放在第一页之外的其他页面。在得出“文件里没有可用资产”的结论之前,务必先枚举全部页面。
3. 脚本必须总是返回一个值
返回值是 Agent 与脚本之间唯一的通信通道。没有 return 时,调用方拿不到任何有效响应。
// WRONG —— 没有 return,调用方得不到有效反馈
figma.createRectangle()
// CORRECT —— 返回结果(对象自动序列化,错误自动捕获)
const rect = figma.createRectangle()
return { nodeId: rect.id }
4. 永远不要使用 figma.notify()
figma.notify() 在 use_figma 中会抛 "not implemented" 错误。反馈信息请通过返回值传回给 Agent。
// WRONG —— 抛 "not implemented" error
figma.notify("Done!")
// CORRECT —— 返回值把数据回传给 Agent
return "Done!"
同理,console.log() 也不会被返回,输出一律走 return。
5. getPluginData() / setPluginData() 不受支持
这两个 API 在 use_figma 中不可用。请改用 受支持的 getSharedPluginData() / setSharedPluginData()(需要命名空间),或通过返回节点 ID 在多次调用间跟踪节点。
// WRONG —— use_figma 不支持
node.setPluginData('my_key', 'my_value')
const val = node.getPluginData('my_key')
// CORRECT —— 使用 shared plugin data(需要命名空间)
node.setSharedPluginData('my_namespace', 'my_key', 'my_value')
const val = node.getSharedPluginData('my_namespace', 'my_key')
// ALSO CORRECT —— 返回节点 ID,跨调用跟踪
const rect = figma.createRectangle()
return { nodeId: rect.id }
// 然后在下一个 use_figma 调用中把 nodeId 作为字符串字面量传入
类型索引 plugin-api-standalone.index.md 的 PluginDataMixin 行也标注了同样的限制:getSharedPluginData/setSharedPluginData 受支持,getPluginData/setPluginData NOT supported。
七、Auto Layout 与尺寸顺序陷阱(含 HUG/FILL 交互)
这是视觉 bug 最集中的一类。核心口诀:先入父容器,再设 FILL;先 resize,再设 sizing mode;不要用垃圾值喂 resize()。
1. 'FILL' 必须先有 auto-layout 父节点
在节点成为 auto-layout frame 的子节点之前设置 layoutSizingHorizontal/Vertical = 'FILL',会抛 "FILL can only be set on children of auto-layout frames"。
// WRONG —— 节点尚未成为 auto-layout frame 的子节点就设 FILL
const child = figma.createFrame()
child.layoutSizingVertical = 'FILL' // ERROR: "FILL can only be set on children of auto-layout frames"
parent.appendChild(child)
// CORRECT —— 先 append 到 auto-layout 父节点,再设 FILL
const child = figma.createFrame()
parent.appendChild(child) // parent 需已设置 layoutMode
child.layoutSizingVertical = 'FILL' // Works!
Tip:需要支持 FILL 子节点的父容器时,优先使用 figma.createAutoLayout()(或 figma.createAutoLayout('VERTICAL'))替代 figma.createFrame()。它返回的 frame 已设置 layoutMode 且两轴为 hug,无需手写属性顺序。
const parent = figma.createAutoLayout() // layoutMode = 'HORIZONTAL', sizing = AUTO
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL' // 立即生效
2. HUG 父容器会压扁 FILL 子节点
HUG 父容器无法给 FILL 子节点有意义的大小。若子节点 layoutSizingHorizontal = "FILL" 而父容器是 "HUG",子节点会塌缩到最小尺寸。要让 FILL 子节点真正撑开,父容器必须是 "FILL" 或 "FIXED"。这是 select 字段、输入框、操作行里文本被截断的常见根因。
// WRONG —— 父容器 hug,FILL 子节点分不到任何额外空间
const parent = figma.createAutoLayout()
parent.layoutSizingHorizontal = 'HUG'
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL' // 塌缩到最小尺寸!
// CORRECT —— 父容器必须是 FIXED 或 FILL,FILL 子节点才能展开
const parent = figma.createAutoLayout()
parent.resize(400, 50)
parent.layoutSizingHorizontal = 'FIXED' // 或处于另一 auto-layout 中时为 'FILL'
const child = figma.createFrame()
parent.appendChild(child)
child.layoutSizingHorizontal = 'FILL' // 撑满剩余 400px
3. hug 父容器搭配 layoutGrow 会导致内容被压缩
当父容器 primaryAxisSizingMode='AUTO'(hug)时,给子节点设 layoutGrow = 1 会让子节点缩小到自然尺寸以下而不是展开,内容被隐藏。
// WRONG —— 父容器 hug 时给子节点 layoutGrow,内容被压缩、子节点被隐藏
const parent = figma.createComponent()
parent.layoutMode = 'VERTICAL'
parent.primaryAxisSizingMode = 'AUTO' // hug contents
const content = figma.createAutoLayout('VERTICAL')
parent.appendChild(content)
content.layoutGrow = 1 // BUG: content 被压缩,子节点不可见!
// CORRECT —— 仅在父容器为 FIXED 且有富余空间时使用 layoutGrow
content.layoutGrow = 0 // 让 content 保持自然尺寸
// 或者:先把父容器改为 FIXED
parent.primaryAxisSizingMode = 'FIXED'
parent.resizeWithoutConstraints(300, 500)
content.layoutGrow = 1 // NOW 正确撑满剩余空间
4. width / height 是只读的 —— 用 resize()
node.width 和 node.height 只读,直接赋值会抛 "TypeError: no setter for property"。请改用 resize() 或 resizeWithoutConstraints()。注意 x / y 不是只读的,可以直接赋值。
// WRONG —— 抛 "no setter for property"
node.width = 300
node.height = 64
// CORRECT —— 用 resize() 改变尺寸
node.resize(300, 64) // 同时改宽高
node.resize(300, node.height) // 只改宽度
node.resize(node.width, 64) // 只改高度
// CORRECT —— x / y 可直接写
node.x = 100
node.y = 200
Sections 与 component sets 请使用 resizeWithoutConstraints() 而不是 resize()(见下文 Sections 小节)。
5. resize() 会把两轴 sizing mode 静默重置为 FIXED
resize(w, h) 会同时把 primaryAxisSizingMode 与 counterAxisSizingMode 重置为 FIXED。如果你先设置了 HUG 再调用 resize(),frame 会被锁定到你传入的精确像素值——即使那只是个随手写的 1。
// WRONG —— 设置 sizing mode 之后调用 resize(),会把模式覆盖回 FIXED
const frame = figma.createComponent()
frame.layoutMode = 'VERTICAL'
frame.primaryAxisSizingMode = 'AUTO' // hug height
frame.counterAxisSizingMode = 'FIXED'
frame.resize(300, 10) // BUG: 两轴都被重置为 'FIXED'!高度永远卡在 10px
// ESPECIALLY DANGEROUS —— 只想管一个轴,却传了垃圾值
const comp = figma.createComponent()
comp.layoutMode = 'VERTICAL'
comp.layoutSizingHorizontal = 'FIXED'
comp.layoutSizingVertical = 'HUG'
comp.resize(280, 1) // BUG: “我只想宽度=280”,结果高度被锁成 1px!
// HUG 被 resize() 重置为 FIXED,frame 永久变成 280×1
// CORRECT —— 先调 resize(),再设置 sizing mode
const frame = figma.createComponent()
frame.layoutMode = 'VERTICAL'
frame.resize(300, 40) // 使用合理默认值,永远不要用 0 或 1
frame.counterAxisSizingMode = 'FIXED' // 宽度保持 300
frame.primaryAxisSizingMode = 'AUTO' // NOW 高度设为 hug —— 这次能保持住!
// 或者用现代简写(等价):
// frame.layoutSizingHorizontal = 'FIXED'
// frame.layoutSizingVertical = 'HUG'
经验法则:对于你计划保持 HUG 的轴,永远不要把随手/垃圾值(如 1 或 0)传给 resize()。要么先 resize() 再设 sizing mode,要么用不会造成视觉 bug 的合理默认值(即使 mode 重置未被察觉)。
6. reparenting 后节点坐标不会自动重置
把一个节点移进新的父节点时,其相对坐标保持不变。若不做显式重置,节点会停留在旧的 (x, y) 位置,出现“错位漂移”。
// WRONG —— 以为移入新父节点后坐标会自动重置
const node = figma.createRectangle()
node.x = 500; node.y = 500;
figma.currentPage.appendChild(node)
section.appendChild(node) // 节点相对 section 仍停留在 (500, 500)!
// CORRECT —— 任何 reparenting 操作之后显式设置 x/y
section.appendChild(node)
node.x = 80; node.y = 80; // 重置到 section 内期望的位置
7. 混合宽度行做网格布局会导致重叠
用固定的“列间距”偏移去排版不同宽度的行,宽行必然溢出碰撞。例如竖版卡片(320px)与横版卡片(500px)混排时,按 370 步进只对 320px 卡片成立。
// WRONG —— 对不同宽度条目使用单一列偏移
// 例如 2 行网格中混排竖版卡片 (320px) 和横版卡片 (500px)
for (let i = 0; i < allCards.length; i++) {
allCards[i].x = (i % 4) * 370 // 对 320px 卡片成立,对 500px 卡片不行!
}
// CORRECT —— 基于实际子节点宽度逐行独立计算间距
const gap = 50
let x = 0
for (const card of horizontalCards) {
card.x = x
x += card.width + gap // 使用实际宽度,而非固定列宽
}
8. Sections 不会自动缩放以适应内容
createSection() 出来的节点保持默认尺寸,内容可能溢出边界。必须添加内容后显式 resize。
// WRONG —— section 保持默认大小,内容溢出
const section = figma.createSection()
section.name = "My Section"
section.appendChild(someNode) // node 可能落在 section 边界之外
// CORRECT —— 添加内容后显式 resize
const section = figma.createSection()
section.name = "My Section"
section.appendChild(someNode)
section.resize(
Math.max(someNode.width + 100, 800),
Math.max(someNode.height + 100, 600)
)
Sections 属于带约束的容器类型,请用 resizeWithoutConstraints() 而非 resize() 来规避约束干扰。
9. counterAxisAlignItems 不支持 'STRETCH'
counterAxisAlignItems 的合法枚举只有 'MIN' | 'MAX' | 'CENTER' | 'BASELINE',传入 'STRETCH' 会抛非法枚举值错误。想要“拉伸”效果的正确姿势是:父容器对齐方式设为 'MIN',再让子节点在交叉轴上 FILL。
// WRONG —— 'STRETCH' 不是合法枚举值
comp.counterAxisAlignItems = 'STRETCH'
// Error: Invalid enum value. Expected 'MIN' | 'MAX' | 'CENTER' | 'BASELINE', received 'STRETCH'
// CORRECT —— 父容器用 'MIN',然后让子节点在交叉轴 FILL
comp.counterAxisAlignItems = 'MIN'
comp.appendChild(child)
// 竖排布局拉伸宽度:
child.layoutSizingHorizontal = 'FILL'
// 横排布局拉伸高度:
child.layoutSizingVertical = 'FILL'
类型声明 plugin-api-standalone.d.ts 中 counterAxisAlignItems 的类型正是 'MIN' | 'MAX' | 'CENTER' | 'BASELINE',从类型层面就没有 'STRETCH'。
八、组件与变体(Component & Variant)陷阱
1. combineAsVariants 只接受 ComponentNode
传入 Frame 会直接报错。先 createComponent(),组件名即变体属性("variant=primary, size=md" 这种命名),再合并。
// WRONG —— 传 frames
const f1 = figma.createFrame()
figma.combineAsVariants([f1], figma.currentPage) // Error!
// CORRECT —— 传 components
const c1 = figma.createComponent()
c1.name = "variant=primary, size=md"
const c2 = figma.createComponent()
c2.name = "variant=secondary, size=md"
figma.combineAsVariants([c1, c2], figma.currentPage)
2. combineAsVariants 在 use_figma 中不会自动布局
合并后所有变体都叠在 (0, 0),生成的 ComponentSet 会小得异常(宽高等于单个变体的尺寸)。必须手动把子变体排成网格,并从实际子节点边界(而非公式)反推 ComponentSet 尺寸——公式误差会让变体留在边界之外。
// WRONG —— 所有变体堆叠在 (0, 0),得到一个极小的 ComponentSet
const components = [comp1, comp2, comp3]
const cs = figma.combineAsVariants(components, figma.currentPage)
// cs.width/height 会是单个变体的大小!
// CORRECT —— 合并后手动把子节点排成网格
const cs = figma.combineAsVariants(components, figma.currentPage)
const colWidth = 120
const rowHeight = 56
cs.children.forEach((child, i) => {
const col = i % numCols
const row = Math.floor(i / numCols)
child.x = col * colWidth
child.y = row * rowHeight
})
// CRITICAL: 从实际子节点边界反推尺寸,而不是用公式
// 公式误差会让变体留在边界之外
let maxX = 0, maxY = 0
for (const child of cs.children) {
maxX = Math.max(maxX, child.x + child.width)
maxY = Math.max(maxY, child.y + child.height)
}
cs.resizeWithoutConstraints(maxX + 40, maxY + 40)
九、字体与排版陷阱
1. lineHeight / letterSpacing 必须是对象,不能是裸数字
两个属性的值都是对象结构。裸数字赋值会抛错或静默无效,同时适用于 TextStyle 和 TextNode 上的属性,且贯穿 use_figma、交互式插件以及任何其他插件 API 上下文。
// WRONG —— 抛错或静默无效
style.lineHeight = 1.5
style.lineHeight = 24
style.letterSpacing = 0
// CORRECT
style.lineHeight = { unit: "AUTO" } // auto/intrinsic
style.lineHeight = { value: 24, unit: "PIXELS" } // 固定像素行高
style.lineHeight = { value: 150, unit: "PERCENT" } // 字号百分比
style.letterSpacing = { value: 0, unit: "PIXELS" } // 无字距
style.letterSpacing = { value: -0.5, unit: "PIXELS" } // 紧凑
style.letterSpacing = { value: 5, unit: "PERCENT" } // 基于百分比
类型层面同样如此:plugin-api-standalone.d.ts 中 LetterSpacing 为 { value: number, unit: 'PIXELS' | 'PERCENT' },LineHeight(L4856 起)为 { value, unit } 或 { unit: 'AUTO' } 的联合类型。
2. 字体样式名随文件而异 —— 用 listAvailableFontsAsync 发现
字体样式名因字体供应商、因每个 Figma 文件而异。永远先调用 figma.listAvailableFontsAsync() 发现精确的样式字符串,再加载——不要靠记忆猜测,也不要用 try/catch 去探测。例如 "SemiBold" 与 "Semi Bold" 的差异就是经典坑位。
完整“发现 + 加载”模式参见技能参考文档 text-style-patterns.md,其示例函数通过 listAvailableFontsAsync 过滤出指定 family 的全部 style:
async function getAvailableFontStyles(family) {
const allFonts = await figma.listAvailableFontsAsync();
return allFonts
.filter(f => f.fontName.family === family)
.map(f => f.fontName.style);
}
同时牢记:任何包含未加载字体节点的操作都需要先加载字体——不仅是设置文本的操作。appendChild、insertChild、setBoundVariable、setExplicitVariableModeForCollection、setValueForMode,甚至 findAll 的回调都可能命中这条限制(技能规则第 8 条)。
十、类型安全与属性写入陷阱
1. 调用类型专属方法前必须做类型守卫
某些方法只存在于特定节点类型上,调用在错误类型上会抛 "TypeError: not a function"。调用前务必先做类型检查。
// WRONG —— node 可能不是 TextNode
const node = await figma.getNodeByIdAsync('952:1253');
const segments = node.getStyledTextSegments(['hyperlink']); // node 不是 TEXT 时抛 TypeError
// CORRECT —— 先检查类型
const node = await figma.getNodeByIdAsync('952:1253');
if (!node || node.type !== 'TEXT') return { error: `Expected TextNode, got ${node?.type ?? 'null'}` };
const segments = node.getStyledTextSegments(['hyperlink']);
常见的类型专属方法与对应节点类型对照:
| 方法 | 需要的节点类型 |
|---|---|
getStyledTextSegments() |
TEXT |
setRangeFontName()、setRangeFontSize() |
TEXT |
createInstance() |
COMPONENT |
addComponentProperty() |
COMPONENT、COMPONENT_SET |
createVariant() |
COMPONENT_SET |
2. 写入不存在的属性会抛 "object is not extensible"
Figma 插件 API 的节点对象不可扩展——不能添加新属性。对节点类型上不存在的属性名做写入会抛 "Cannot add property X, object is not extensible"(表现常为 "object is not extensible")。只有写操作会触发,且只针对该节点类型未定义的属性。
// WRONG —— VectorNode 上没有 'strokeDashes';抛 "object is not extensible"
const v = figma.createVector()
v.strokeDashes = [4, 8] // Error!
// CORRECT —— 真正的属性是 dashPattern
v.dashPattern = [4, 8]
// WRONG —— 任何臆造属性名都会抛同样的错误
node.customColor = '#ff0000' // Error —— 不是真实 API 属性
规避方法:设置任何属性前,先在 plugin-api-standalone.d.ts 中确认该属性存在于对应节点类型上——听起来合理但不在类型定义里的属性名一定会抛错。(注:类型文件中 strokeDashes 确实不存在于 Vector 图形节点,dashPattern 才是其描边虚线属性。)
3. detachInstance() 会使祖先节点 ID 失效
对嵌套在库组件实例内部的子实例调用 detachInstance() 时,父实例可能被隐式一并 detach(从 INSTANCE 变成 FRAME,并获得新 ID)。任何之前缓存的父 ID 都会随之失效,getNodeByIdAsync 将返回 null。
// WRONG —— 子节点 detach 后仍使用缓存的父 ID
const parentId = parentInstance.id;
nestedChild.detachInstance();
const parent = await figma.getNodeByIdAsync(parentId); // null! ID 已改变
// CORRECT —— 从稳定的(非 instance)frame 出发重新遍历发现
const stableFrame = await figma.getNodeByIdAsync(manualFrameId);
nestedChild.detachInstance();
const parent = stableFrame.findOne(n => n.name === "ParentName");
若要对多个同层嵌套实例执行 detach,请在单个 use_figma 调用内完成——在任一次 detach 变更节点树之前,先通过遍历把所有目标节点都发现好。
十一、使用前的自检清单与错误恢复
上述陷阱可以浓缩为一张每次提交 use_figma 调用前的速查清单(技能 SKILL.md 第 8 节的节选):
- [ ] 代码用
return回传数据(不要figma.closePlugin(),也不要包 async IIFE,脚本已被自动包裹) - [ ] 颜色一律 0–1(不是 0–255);paint
color只含{r, g, b},opacity放 paint 层 - [ ] fills/strokes 以新数组重新赋值,绝不在原处修改
- [ ] 切页用
await figma.setCurrentPageAsync(page)(同步 setter 不可用) - [ ]
layoutSizingX = 'FILL'在parent.appendChild(child)之后设置 - [ ]
resize()在任何 sizing mode 设置之前调用(resize 会把模式重置为 FIXED) - [ ]
loadFontAsync()在一切文本相关操作前调用;样式名经listAvailableFontsAsync核实,不靠记忆 - [ ]
lineHeight/letterSpacing使用{unit, value}对象(非裸数字) - [ ] 新建顶层节点避开 (0,0)
- [ ] 返回值包含全部新建/变更节点 ID
- [ ] 每个 Promise 都
await,杜绝 fire-and-forget
当脚本报错时,请遵循“原子性”认知:use_figma 失败脚本是原子的——出错时脚本完全不会执行,文件保持调用前状态,不会留下半成品节点。因此正确的做法是先 STOP,仔细阅读错误信息,必要时用 get_metadata/get_screenshot 观察当前文件状态,修复脚本后再重试。常见错误与对策映射(技能自愈模式节选):
| 错误信息 | 可能原因 | 修复方式 |
|---|---|---|
"not implemented" |
使用了 figma.notify() |
删除,改用 return 输出 |
"node must be an auto-layout frame..." |
在 append 前设置了 FILL/HUG |
把 appendChild 移到 layoutSizingX = 'FILL' 之前 |
"Setting figma.currentPage is not supported" |
使用了同步页面 setter | 改用 await figma.setCurrentPageAsync(page) |
| 属性值越界 | 颜色通道 > 1(0–255 误写成 0–1) | 除以 255 |
"Cannot read properties of null" |
节点不存在(ID 错误、页面错误) | 检查页面上下文与 ID |
| 脚本卡死 / 无响应 | 死循环或未 resolve 的 Promise | 检查 while(true)、补齐 await |
"The node with id X does not exist" |
父实例被子节点 detachInstance() 隐式 detach,ID 已变 |
从稳定的(非 instance)父 frame 重新遍历发现 |
详细的逐文件说明、get_metadata/get_screenshot 校验工作流与恢复步骤,见技能参考文档 validation-and-recovery.md;组件/变体方向的更多示例见 component-patterns.md,变量方向见 variable-patterns.md,文本样式方向见 text-style-patterns.md。在仓库中按需查阅这些文档,配合 plugin-api-standalone.d.ts 的类型定义,即可把 Figma 脚本调试效率提升一个量级。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00