Community编程与开发github.com

figma-build

构建Figma和FigJam插件内容——那些静默失败的尺寸和字体规则、阻止单一错误值导致构建崩溃的保护钳、通过REST API读取评论以及告知你代码是否实际运行的循环。在编写或调试Figma插件、以编程方式生成画框或线框图、或当构建中途中止、布局崩溃、文本折行为每行一个字符、画框与页面上已有内容重叠、或“看起来没变”时使用。

figma-build 是什么?

该技能文档汇编了Figma插件开发中一系列静默失败的模式及应对策略,涵盖了构建循环、值保护、布局与排版规则、字体加载、本地模拟、评论读取等关键环节。其核心价值在于将那些“不抛异常、不告警、仅导致画布异常”的隐性错误显性化,提供可复用的代码片段与诊断方法。支持的平台包括Figma插件开发环境(Plugin API)以及通过REST API读取评论的Python工具。工作原理上,它强调在代码中嵌入构建标记(BUILD stamp)以验证包是否被缓存、使用钳位函数拦截API拒绝的边界值、遵循严格的文本尺寸设置顺序,并提供本地桩(stub)模拟Figma全局对象的严格验证。此外,文档详细描述了通过REST API读取评论的完整流程,包括获取、映射、原地修改与记录处理状态的“评论循环”,确保审阅反馈不被误删或重复应用。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ABarracu87/figma-build-skill/tree/HEAD

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Figma 构建

这里的一切都曾静默失败。没有抛出异常,没有警告,画布只是看起来不对劲。

循环,优先于一切

标记构建并清空自己的输出。 否则,当Figma提供的是缓存bundle时,你会花很长时间调试“看起来一样”。

const BUILD = "15:03";                 // 每次bundle时递增
function clearPrevious() {
  const mine = figma.currentPage.children.filter(n => /^\d\d · /.test(n.name));
  mine.forEach(n => n.remove());
  return mine.length;
}
figma.closePlugin("Build " + BUILD + " — replaced " + removed + " frames.");

toast中的两个数字都具有诊断意义。过时的印记意味着bundle被缓存:删除插件、重新导入manifest.json、重新打开标签页。当画布上明显有画框时显示Replaced 0,也意味着同样的问题。

没有这个,一个重新创建画框的插件会在第一组之上堆叠第二组,而且没有任何信息告诉你你正在看哪一组。计算画框名称的副本数量是证明运行是否发生的方式。

“清除并重建”仅用于首次构建。 一旦文件进入审查阶段,评论会绑定到节点ID。删除一个画框并绘制新的会使其上的所有评论成为孤儿。在审查轮次中,应原地编辑现有节点(参见 审查循环)。


一个错误值摧毁整个构建

此文件中的每个失败都有相同的形式:API拒绝的单个值,在绘制过程中抛出,留下一个半渲染的页面,这会被解读为布局问题。典型元凶:负宽度、固定的10px文本高度、未加载的字重。

在边界处钳制,而不是信任算术。

function W1(v, min){ const n = Number(v); return (isFinite(n) && n > 0) ? n : (min || 1); }

所有宽度、高度和文本宽度都经过它。一次钳制代价是一个看起来错误的框;一次抛出代价是整个页面。

咬人的算术是仅在一个宽度下成立的固定减法:

const FEED = DW - PAD*2 - 400;   // 1440时是840。390时是MINUS 50。

任何从画布宽度减去的常量都需要一个断点守卫,否则在小的画布上它会变成负数。

要识别的症状: 宽画框看起来完成,窄画框缺失或半绘制,与某些东西重叠。这不是定位错误。读取错误toast——它会指明属性和节点。


永远不要从原点开始绘制

从(0, 0)开始的插件会落到已经存在的内容之上,而且一个仅移除自身画框的清理器救不了你:共享一个页面的两个插件都认为画布是空的。

先测量,然后开始清理:

let startY = 0;
figma.currentPage.children.forEach(n => { startY = Math.max(startY, n.y + n.height); });
if (figma.currentPage.children.length) startY += 600;

在画布上标记每一行,并让清理器匹配构建产生的所有东西——画框和标签——否则重新运行会留下孤立的文本。


文本尺寸设置,严格按照此顺序

最昂贵的错误之一。Figma在其内容改变时重新计算文本节点的高度,而不是在你调整其大小时。

const str = String(s);
const t = figma.createText();
t.fontName = { family: "Inter", style: weight };
t.characters = str;                    // 1. 首先
t.fontSize = size;                     // 2. 然后设置样式
t.lineHeight = { unit: "PERCENT", value: 145 };
if (width) {
  t.textAutoResize = "HEIGHT";         // 3. 在空节点上不会生效
  t.resize(width, t.height);           //    永远不要传递字面量高度
  t.characters = str;                  // 4. 强制在新宽度下重新测量
} else {
  t.textAutoResize = "WIDTH_AND_HEIGHT";
}
  • 在节点有字符之前设置textAutoResize = "HEIGHT"会被忽略。将内容放到最后而不移动该属性会使情况更糟。
  • resize(w, 10) 将高度永远固定在10。传递t.height。
  • 重新赋值相同的字符串是双重保险的重新测量。

症状: 所有具有显式宽度的文本大约10px高,没有显式宽度的文本正确,且所有高度仅来自文本的画框会折叠到其内边距。


width 意味着固定宽度——哪个轴向并不明显

if (opts.horizontal) {
  f.primaryAxisSizingMode = width ? "FIXED" : "AUTO";   // width 是主轴
  f.counterAxisSizingMode = "AUTO";
} else {
  f.counterAxisSizingMode = width ? "FIXED" : "AUTO";   // width 是交叉轴
  f.primaryAxisSizingMode = "AUTO";
}

在水平画框上设置counterAxisSizingMode会固定其高度并让宽度自动包裹。以这种方式构建的每一行都没有多余的宽度,因此没有子元素可以增长进去。

在设置尺寸模式之前调整大小,否则显式大小会与自动包裹冲突。


填充行的列

function growCol(n) { n.layoutGrow = 1; n.counterAxisSizingMode = "FIXED"; return n; }

以width: null创建的垂直画框会自动包裹。赋予它layoutGrow = 1和STRETCH子元素,你就有了一个循环约束:父元素根据子元素大小调整,子元素根据父元素大小调整。Figma会以其最小值来解决。

症状: 卡片大约45px宽,正文每个字符一行,文本节点报告1 x 988。页面画框比应有高度高三倍,完全来自虚假的垂直空间。

优先向行传递显式宽度,而不是依赖STRETCH来解决。


测量,不要用眼睛看

几何信息比截图更快地回答了问题,并且它暴露了上述的所有错误。拉取get_metadata,然后:

  • 高度 ≤ 12px → 文本或画框折叠
  • 宽度为1 → 上述循环约束
  • 页面远高于其HTML等效项 → 窄换行,而非内容
  • 当你期望两个副本时只有一个画框副本 → 运行从未发生

完整性检查总数:块的y范围应无间隙地平铺其页面,最后一个块应恰好结束于页面高度处。


本地模拟插件

技能文件夹中的 tools/figma_stub.js 是一个准备好的严格模拟;node tools/test_figma_stub.js 证明它能在Figma抛出异常的地方抛出。

figma 全局对象的模拟会捕获抛出、null追加和结构错误,早于往返测试。它无法模拟文本度量,因此它证明代码能运行,但绝不证明布局是正确的。

让模拟像真实API一样严格,否则它会通过那些在Figma中会失败的构建。以下是每个都是因为模拟过于宽松而到达画布的错误:

resize(w, h){
  if (!(w >= 0)) throw new Error('在 resize 中:属性 "width" 未能通过验证');
  if (!(h >= 0)) throw new Error('在 resize 中:属性 "height" 未能通过验证');
  ...
}
set fontName(v){
  if (!loaded.has(v.family + "|" + v.style))
    throw new Error('无法使用未加载的字体系 "' + v.family + " " + v.style + '"');
  ...
}
appendChild(c){ if (!c) throw new Error("null 子元素"); ... }

为模拟提供 currentPage.children,并用真实页面已经包含的画框来初始化它——这就是原点冲突如何在本地显现的方式。

为了检查定位,模拟还需要计算自动布局高度:对于VERTICAL画框,求和子元素加上 itemSpacing 和内边距;对于HORIZONTAL画框,取最大值;当 primaryAxisSizingMode === "FIXED" 时返回固定值。粗略模拟足以捕获行重叠。

运行每个调度路径、每个断点和每种字体设置,而不仅仅是正常路径:

仅 Inter · Inter + 等宽 · 全部三种 · 仅 Inter Regular
desktop · mobile     dark · light

字体:加载你将要求的,解析你得到的

分配未加载的字体会抛出。背后有两个陷阱:

  • 你必须预加载代码要求的每个字重。 加载 Regular、Medium 和 Bold 然后请求 Semi Bold 是崩溃,等待使用它的卡片。
  • 样式名称因字体系列而异。 Inter 使用 "Semi Bold";JetBrains Mono 使用 "SemiBold"。用错误的拼写请求正确的字重仍然会抛出。

分别探测每个字重,记录实际加载的内容,并通过回退链解析每个请求,而不是假设:

const ALIAS = { "Semi Bold": ["Semi Bold","SemiBold","Medium","Bold","Regular"], … };
function font(family, style){
  for (const s of ALIAS[style]) if (LOADED[family+"|"+s]) return { family, style:s };
  for (const s of ALIAS[style]) if (LOADED["Inter|"+s]) return { family:"Inter", style:s };
  return { family:"Inter", style:"Regular" };
}

永远不要在入口点使用 Promise.all 严格预加载——缺失的一个字重会杀死一个解析器本可以处理的构建。在关闭toast中报告你解析到的字体系列。


一个插件,一项任务

抵制在一个插件内部使用调度开关——基于编辑器类型、页面名称或选区。页面名称切换意味着用户必须执行一个仪式才能使事情工作,而当它静默地做错误的事情时,没有什么可读取的。

两个具有不同 id 和 name 值的manifest.json成本为零,并消除了整个问题类别。当重新导入的manifest携带一个它已知的 id 时,Figma还会重用缓存的注册,因此更改 id 是强制干净加载的方法。


读取评论

MCP 暴露了 get_metadata、get_design_context、get_screenshot、get_figjam、use_figma —— 它们都不能读取评论,而且Plugin API也没有评论访问权限。评论存在于REST API中。

使用技能文件夹中的 tools/figma_comments.py(Python 3,标准库):

T=~/.claude/skills/figma-build/tools/figma_comments.py
python3 $T check <文件-url>            # 正常:令牌可以读取评论 (文件KEY上N个)
python3 $T fetch <文件-url>            # 开放线程为JSON,过滤掉已解决和日志记录
python3 $T mark  <文件-url> ID... --change "改变的内容"
python3 $T reply <文件-url> ROOT_ID "完成:logo 增大20%"   # 需要写入作用域

它从 ~/.figma-token 读取令牌,并且永远不会打印它。从工作文件夹运行,以便日志记录文件存放在那里,或者设置 FIGMA_LEDGER。没有删除命令。如果报告SSL证书错误,说明Python没有根证书:改用 /usr/bin/python3,或者 pip install certifi。

作用域:文件评论 → 读取。将令牌存储在文件中(~/.figma-token,权限600),永远不要存储在对话中。如果此人无法运行shell命令,则写入一个包含占位符的文件,并用 open -a TextEdit 打开让其粘贴。

/v1/me 在仅评论作用域的令牌上返回 403。这是预期的,不是令牌损坏。/v1/files/:key/nodes 在没有 file_content:read 作用域时也会返回403。

每个评论都带有 client_meta:

{ "node_id": "2:95", "node_offset": { "x": 279, "y": 1257 } }

通过拉取 get_metadata 并找到其y范围包含偏移量的子画框,将其映射到一个块。线程以扁平形式返回——通过 parent_id 分组,并且回复通常携带实际指令,而根评论仅是一个指针。

评论是针对文件的。将内容移动到新文件会使它们丢失。

审查循环,当被要求“阅读我的评论”时

  1. 获取 使用 figma_comments.py fetch,它会将评论分组为线程。跳过已解决的(resolved_at 已设置)以及已经存在于处理日志(下文)中的每个ID。旧评论会一直保留在文件中直到被解决,所以没有这两个过滤器,每次审查以及之后的每次对话都会重新应用上一轮的反馈。
  2. 映射 通过 client_meta 将每个线程映射到其画框,如上所述。
  3. 在绘制前确认: 在一个简短的表格中列出每条评论、其映射到的画框以及计划的更改。钉在空白画布上的评论,或者模棱两可的评论,应该提问而不是猜测。
  4. 应用 更改原地:改变现有节点的属性,保持其ID不变。永远不要删除和重绘被评论的画框,因为那会使它的评论成为孤儿。通过MCP(use_figma),画布直接更新。通过开发插件,在磁盘上编辑其代码,递增构建标记,然后请此人重新运行插件。
  5. 验证 使用 get_metadata 和截图(参见测量,不要用眼睛看)。在插件路径上,在信任画布之前检查关闭toast是否显示新的标记。
  6. 逐条报告 哪些评论改变了什么。如果令牌具有写入作用域且此人希望,则在每个线程中回复。
  7. 记录 在回合结束前,使用 figma_comments.py mark 记录每个已处理的评论ID(线程根和回复),包括那些你仅回答了问题的。
  8. 永远不要删除评论,即使被要求清理。它们是审查记录。旧反馈通过日志记录(而非删除)在后续轮次中被排除。如果某人需要在Figma中清理一轮,由他们自己解决评论。

处理日志。 一个在工作目录旁边的JSON文件:在项目文件夹中为 figma/comments-handled.json,否则为工作目录中的 figma-comments-handled.json。在每次审查开始时读取它,包括在新的对话中。

{ "<文件KEY>": { "<评论ID>": { "handled_at": "2026-11-03T14:05:00Z", "change": "logo 在1080x1920上增大20%" } } }

没有用于解决评论的API端点,因此解决操作由用户手动完成。

何时请求重新导入(仅插件路径):当toast显示过时的标记,或“replaced 0”而画框可见时。请他们删除插件,重新导入其 manifest.json 并重新打开标签页。如果失败,更改manifest的 id(参见一个插件,一项任务)。


大规模截图

每个页面一次截图加上局部裁剪,优于每个块一次调用。块的y坐标和高度来自 get_metadata,maxDimension 设置等于或高于画框高度会给出1:1的比例,并且裁剪会精确对齐。

相关技能