Figma ビルド
ここに書かれていることはすべて、最初は何のエラーも出さずに失敗しました。何もスローされず、何も警告されず、キャンバスがただおかしく見えるだけです。
何よりも先に、ループ
ビルドにスタンプを押し、自身の出力をクリアしてください。 そうしないと、Figma がキャッシュされたバンドルを提供しているときに、「見た目が同じだ」というデバッグに長時間費やすことになります。
const BUILD = "15:03"; // バンドルごとに更新
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.");
トースト内の2つの数値は、どちらも診断用です。スタンプが古い場合は、バンドルがキャッシュされています。プラグインを削除し、マニフェストを再インポートして、タブを開き直してください。キャンバス上にフレームが表示されているのに Replaced 0 と表示される場合も同じことを意味します。
これがないと、フレームを再作成するプラグインは、最初のセットの上に2番目のセットを重ねてしまい、どちらを見ているのかを示すものは何もありません。フレーム名のコピーを数えることは、実行が実際に行われたかどうかを証明する方法です。
クリア&リビルドは最初のビルドのみに使用してください。 ファイルがレビュー中になると、コメントはノード ID にピン留めされます。フレームを削除して新しいフレームを描画すると、そのフレーム上のすべてのコメントが孤立します。レビューラウンド中は、既存のノードをインプレースで編集してください(レビューループ を参照)。
1つの不正な値がビルド全体を台無しにする
このファイルのすべての失敗は同じ形をしています。API が拒否する単一の値が、描画の途中でスローされ、レイアウトの問題として読める半分レンダリングされたページを残します。典型的な原因としては、負の幅、固定された10pxのテキストの高さ、ロードされていないフォントウェイトなどがあります。
計算を信頼するのではなく、境界でクランプしてください。
function W1(v, min){ const n = Number(v); return (isFinite(n) && n > 0) ? n : (min || 1); }
すべての幅、高さ、テキスト幅をこれに通します。クランプによるコストは、見た目がおかしいボックス1つだけです。スローによるコストはページ全体です。
問題を引き起こす計算は、特定の幅でしか成立しない固定の減算です。
const FEED = DW - PAD*2 - 400; // 1440 では 840。 390 では -50。
キャンバス幅から定数を減算する場合は、ブレークポイントガードが必要です。そうしないと、小さい画面で負の値になります。
認識すべき症状: 広いフレームは完成しているように見え、狭いフレームは存在しないか、半分描画されていて、他の何かに重なっている。それは位置のバグではありません。エラートーストを読んでください。プロパティとノードが示されています。
原点には決して描画しない
(0, 0) から始まるプラグインは、すでにあるものの上に配置され、自身のフレームのみを削除するクリーナーでは救えません。ページを共有する2つのプラグインは、両方ともキャンバスが空であると信じています。
最初に測定し、それからクリアを開始します:
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行に1文字で折り返され、テキストノードが 1 x 988 と報告される。ページフレームが本来あるべき高さの3倍になり、すべて偽の垂直スペースが原因。
STRETCH に依存して解決するよりも、明示的な幅を行に渡すことを推奨します。
見るのではなく、計測する
ジオメトリはスクリーンショットよりも速く質問に答え、上記のすべてのバグを明らかにします。get_metadata を取得し、次に:
- 高さ ≤ 12px → テキストまたはフレームの崩壊
- 幅が 1 → 上記の循環制約
- ページが HTML の同等物よりもはるかに高い → コンテンツではなく、狭い折り返し
- 予想した2つのコピーの代わりに各フレームが1つだけ → 実行が行われなかった
合計を健全性チェックします。ブロックの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('in resize: Property "width" failed validation');
if (!(h >= 0)) throw new Error('in resize: Property "height" failed validation');
...
}
set fontName(v){
if (!loaded.has(v.family + "|" + v.style))
throw new Error('Cannot use unloaded font "' + v.family + " " + v.style + '"');
...
}
appendChild(c){ if (!c) throw new Error("null child"); ... }
スタブに currentPage.children を与え、実際のページがすでに保持しているフレームをシードとして設定してください — これにより、原点の衝突がローカルでどのように現れるかを確認できます。
位置をチェックするために、スタブは自動レイアウトの高さも計算する必要があります。VERTICAL フレームの場合は子の合計に itemSpacing とパディングを加算し、HORIZONTAL フレームの場合は最大値を取得し、primaryAxisSizingMode === "FIXED" の場合は固定値を返します。大まかでも行の重複をキャッチするには十分です。
すべてのディスパッチパス、すべてのブレークポイント、すべてのフォント設定を実行してください。ハッピーパスだけでなく:
Inter のみ · Inter + mono · すべて3つ · Inter Regular のみ
desktop · mobile dark · light
フォント: 要求するものをロードし、取得したものを解決する
ロードされていないフォントを割り当てるとエラーがスローされます。その背後にある2つの落とし穴:
- コードが要求するすべてのウェイトを事前にロードする必要があります。 Regular、Medium、Bold をロードしてから Semi Bold を要求すると、それを使用する1枚のカードを待ち受けているクラッシュになります。
- スタイル名はファミリーによって異なります。 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 を使用して事前ロードを厳密に行わないでください。1つのウェイトが欠落していると、リゾルバが処理できたはずのビルドが強制終了されます。解決したファミリーを終了トーストで報告してください。
1つのプラグイン、1つのジョブ
単一のプラグイン内で、エディタタイプ、ページ名、または選択に基づくディスパッチスイッチに抵抗してください。ページ名スイッチは、ユーザーが機能する前に儀式を実行する必要があることを意味し、静かに間違ったことをしても、読むべきものが何もありません。
異なる id と name 値を持つ2つのマニフェストはコストがかからず、問題のクラス全体を取り除きます。Figma はまた、再インポートされたマニフェストがすでに認識している id を持っている場合、キャッシュされた登録を再利用するため、id を変更することがクリーンロードを強制する方法です。
コメントの読み取り
MCP は get_metadata、get_design_context、get_screenshot、get_figjam、use_figma を公開しています — どれもコメントを読み取らず、プラグイン API にもコメントへのアクセス権はありません。コメントは REST API にあります。
このスキルフォルダの tools/figma_comments.py を使用してください(Python 3、標準ライブラリ):
T=~/.claude/skills/figma-build/tools/figma_comments.py
python3 $T check <file-url> # OK: トークンはコメントを読み取れます(ファイル KEY の N)
python3 $T fetch <file-url> # オープンスレッドを JSON で取得、仕訳帳と解決済みは除外
python3 $T mark <file-url> ID... --change "what changed"
python3 $T reply <file-url> ROOT_ID "Done: logo 20% bigger" # 書き込みスコープが必要
~/.figma-token からトークンを読み取り、決して表示しません。仕訳帳を作業フォルダに配置するためにそこから実行するか、FIGMA_LEDGER を設定してください。削除コマンドはありません。SSL 証明書の失敗を報告する場合、Python にルート証明書がありません。代わりに /usr/bin/python3 を実行するか、pip install certifi を実行してください。
スコープ: File comments → Read。トークンをファイル(~/.figma-token、chmod 600)に保存し、会話内には決して入れないでください。ユーザーがシェルコマンドを実行できない場合は、プレースホルダー付きのファイルを作成し、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 でグループ化し、ルートが単なるポインタである場合、返信には実際の指示が含まれていることがよくあります。
コメントはファイルごとにあります。コンテンツを新しいファイルに移動すると、それらは残ります。
レビューループ、「コメントを読んで」と依頼された場合
- 取得
figma_comments.py fetchを使用して取得し、コメントをスレッドにグループ化します。解決済みのもの(resolved_atが設定されている)と、すでに処理済み仕訳帳にあるすべての ID をスキップします(以下を参照)。古いコメントは解決されるまでファイルに残るため、これら2つのフィルターがないと、すべてのレビュー、およびその後の会話で、前回のラウンドのフィードバックが再適用されます。 - マッピング 各スレッドを、上記の
client_metaを通じてそのフレームにマッピングします。 - 描画前に確認: 各コメント、それがマッピングされるフレーム、および計画された変更を、1つの短いテーブルにリストします。空のキャンバスにピン留めされたコメント、またはあいまいなコメントについては、推測する代わりに質問をします。
- 適用 変更をインプレースで適用します。既存のノードのプロパティを変更し、その ID を保持します。コメントされたフレームを削除して再描画しないでください。コメントが孤立するためです。MCP(
use_figma)を介して、キャンバスは直接更新されます。開発プラグインを介して、ディスク上のコードを編集し、ビルドスタンプを更新し、ユーザーにプラグインを再度実行するように依頼します。 - 確認
get_metadataとスクリーンショットを使用して確認します(見るのではなく、計測する を参照)。プラグインルートでは、キャンバスを信頼する前に、終了トーストに新しいスタンプが表示されていることを確認します。 - 報告 コメントごとに何が変更されたかを報告します。トークンに書き込みスコープがあり、ユーザーが希望する場合は、各スレッドで返信します。
- 記録 処理したすべてのコメント ID(スレッドルートと返信)を、ターンが終了する前に
figma_comments.py markで記録します。質問で回答したものも含みます。 - コメントを決して削除しない たとえクリーンアップを依頼されても。それらはレビューの記録です。古いフィードバックは、削除するのではなく、仕訳帳を通じて後のラウンドから除外されます。ラウンドを Figma でクリアする必要がある場合、ユーザーがコメントを解決します。
処理済み仕訳帳。 作業ディレクトリ内の JSON ファイル。キャンペーンフォルダの場合は figma/comments-handled.json、それ以外の場合は figma-comments-handled.json。新しい会話も含め、すべてのレビューの開始時にそれを読み取ります。
{ "<file_key>": { "<comment_id>": { "handled_at": "2026-11-03T14:05:00Z", "change": "logo 20% bigger on 1080x1920" } } }
コメントを解決する API エンドポイントはないため、解決はユーザーに委ねられます。
再インポートを依頼するタイミング(プラグインルートのみ): トーストに古いスタンプが表示されている場合、またはフレームが表示されているのに「replaced 0」と表示されている場合。プラグインを削除し、その manifest.json を再度インポートして、タブを開き直すように依頼します。それでも失敗する場合は、マニフェストの id を変更します(1つのプラグイン、1つのジョブ を参照)。
スケールでのスクリーンショット
ブロックごとに1回の呼び出しよりも、ページごとに1回のキャプチャとローカルトリミングの方が優れています。ブロックの y/高さは get_metadata から取得され、maxDimension がフレームの高さ以上であれば 1:1 になり、トリミングは正確に一致します。