Build Figma
Tout ce qui est décrit ici a d'abord échoué en silence. Rien ne lève d'exception, rien n'avertit, et le canvas semble simplement incorrect.
La boucle, avant toute chose
Tamponnez la build et effacez votre propre sortie. Sinon, vous risquez de passer du temps à déboguer « ça a l'air pareil » alors que Figma sert un bundle mis en cache.
const BUILD = "15:03"; // incrémenter à chaque 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.");
Deux nombres dans le toast, et les deux sont des diagnostics. Un tampon obsolète signifie que le bundle
est en cache : supprimez le plugin, réimportez le manifeste, rouvrez l'onglet.
Replaced 0 avec des frames visibles sur le canvas signifie la même chose.
Sans cela, un plugin qui recrée des frames en empile une seconde série par-dessus la première et rien ne vous indique laquelle vous regardez. Compter les copies d'un nom de frame est la manière de prouver si une exécution a eu lieu.
Effacement et reconstruction est réservé aux premières builds seulement. Une fois qu'un fichier est en relecture, les commentaires sont épinglés aux identifiants de nœuds. Supprimer une frame et en dessiner une nouvelle orpheline chaque commentaire qui y est attaché. Pendant un cycle de relecture, modifiez les nœuds existants sur place (voir La boucle de relecture).
Une seule valeur mauvaise casse toute la build
Chaque échec dans ce fichier a la même forme : une seule valeur que l'API rejette, levée en plein dessin, laissant une page à moitié rendue qui ressemble à un problème de mise en page. Coupables typiques : une largeur négative, une hauteur de texte fixée à 10 px, un poids de police non chargé.
Limitez à la frontière plutôt que de faire confiance à l'arithmétique.
function W1(v, min){ const n = Number(v); return (isFinite(n) && n > 0) ? n : (min || 1); }
Faites passer chaque largeur, hauteur et largeur de texte par cette fonction. Une limite coûte une boîte mal placée ; une exception coûte la page.
L'arithmétique qui fait mal est une soustraction fixe qui n'est valable qu'à une seule largeur :
const FEED = DW - PAD*2 - 400; // 840 à 1440. MOINS 50 à 390.
Toute constante soustraite de la largeur du canvas a besoin d'une garde de point de rupture, ou elle devient négative sur la petite largeur.
Symptôme à reconnaître : les grandes frames semblent terminées et les étroites sont absentes ou à moitié dessinées, chevauchant autre chose. Ce n'est pas un bug de positionnement. Lisez le toast d'erreur — il nomme la propriété et le nœud.
Ne dessinez jamais à l'origine
Un plugin qui commence à (0, 0) atterrit par-dessus ce qui est déjà présent, et
un nettoyeur qui ne supprime que ses propres frames ne vous sauvera pas : deux plugins
partageant une page croient tous deux que le canvas est vide.
Mesurez d'abord, puis commencez proprement :
let startY = 0;
figma.currentPage.children.forEach(n => { startY = Math.max(startY, n.y + n.height); });
if (figma.currentPage.children.length) startY += 600;
Étiquetez chaque ligne sur le canvas, et faites en sorte que le nettoyeur corresponde à tout ce que la build émet — frames et étiquettes — sinon une réexécution laisse du texte orphelin derrière elle.
Dimensionnement du texte, dans cet ordre exact
L'erreur la plus coûteuse. Figma recalcule la hauteur d'un nœud de texte lorsque son contenu change, pas lorsque vous le redimensionnez.
const str = String(s);
const t = figma.createText();
t.fontName = { family: "Inter", style: weight };
t.characters = str; // 1. D'ABORD
t.fontSize = size; // 2. styles ensuite
t.lineHeight = { unit: "PERCENT", value: 145 };
if (width) {
t.textAutoResize = "HEIGHT"; // 3. ne s'applique PAS sur un nœud vide
t.resize(width, t.height); // jamais une hauteur littérale
t.characters = str; // 4. force une remesure à la nouvelle largeur
} else {
t.textAutoResize = "WIDTH_AND_HEIGHT";
}
textAutoResize = "HEIGHT"défini avant que le nœud n'ait des caractères est ignoré. Réordonner pour mettre le contenu en dernier sans déplacer la propriété aggrave les choses.resize(w, 10)fige la hauteur à 10 pour toujours. Passezt.height.- Réaffecter la même chaîne est la mesure de sécurité redondante pour forcer une remesure.
Symptôme : chaque texte avec une largeur explicite a une hauteur d'environ 10 px, chaque texte sans largeur explicite est correct, et chaque frame dont la hauteur ne provient que du texte s'effondre à son rembourrage.
width signifie largeur fixe — l'axe concerné n'est pas évident
if (opts.horizontal) {
f.primaryAxisSizingMode = width ? "FIXED" : "AUTO"; // width EST l'axe primaire
f.counterAxisSizingMode = "AUTO";
} else {
f.counterAxisSizingMode = width ? "FIXED" : "AUTO"; // width est le contre-axe
f.primaryAxisSizingMode = "AUTO";
}
Définir counterAxisSizingMode sur une frame horizontale fixe sa hauteur et
laisse la largeur s'ajuster au contenu. Chaque ligne construite de cette façon n'a pas de largeur libre, donc aucun
enfant ne peut s'y développer.
Redimensionnez avant de définir les modes de dimensionnement, sinon la taille explicite entre en conflit avec l'ajustement au contenu.
Une colonne qui remplit une ligne
function growCol(n) { n.layoutGrow = 1; n.counterAxisSizingMode = "FIXED"; return n; }
Une frame verticale créée avec width: null s'ajuste au contenu. Donnez-lui layoutGrow = 1 et
des enfants STRETCH (étirement) et vous avez une contrainte circulaire : le parent se dimensionne aux
enfants, les enfants se dimensionnent au parent. Figma la résout au minimum.
Symptôme : cartes d'environ 45 px de large, copie de corps s'enroulant un caractère par ligne,
neuds de texte rapportant 1 x 988. Frames de page trois fois plus hautes qu'elles ne devraient
l'être, entièrement à cause d'un espace vertical parasite.
Préférez passer des largeurs explicites aux lignes plutôt que de compter sur STRETCH pour résoudre le problème.
Mesurez, ne regardez pas
La géométrie répond à la question plus vite qu'une capture d'écran, et elle expose tous les bugs
ci-dessus. Extrayez get_metadata, puis :
- hauteurs ≤ 12 px → texte ou frames effondrés
- largeurs de 1 → la contrainte circulaire ci-dessus
- une page bien plus haute que son équivalent HTML → enroulement étroit, pas du contenu
- une copie de chaque frame alors que vous en attendiez deux → l'exécution n'a jamais eu lieu
Vérifiez les totaux : les plages y d'un bloc doivent carreler sa page sans espace, et le dernier bloc doit se terminer exactement à la hauteur de la page.
Simuler le plugin localement
tools/figma_stub.js dans ce skill est un stub strict prêt à l'emploi ; node tools/test_figma_stub.js prouve qu'il lève encore des exceptions là où Figma le fait.
Un stub de la variable globale figma attrape les exceptions, les ajouts de null et les erreurs
structurelles avant un aller-retour. Il ne peut pas émuler les métriques de texte, donc il prouve que le
code s'exécute, jamais que la mise en page est correcte.
Rendez le stub aussi strict que l'API réelle, sinon il laissera passer des builds qui échouent dans Figma. Chacune de ces erreurs est un bug qui a atteint le canvas parce que le stub était tolérant :
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"); ... }
Donnez au stub currentPage.children, et initialisez-le avec les frames que la vraie page
contient déjà — c'est ainsi qu'une collision d'origine se manifeste localement.
Pour vérifier le positionnement, le stub doit également calculer les hauteurs de mise en page automatique : additionner
les enfants plus itemSpacing et le rembourrage pour une frame VERTICALE, prendre le maximum
pour une HORIZONTALE, et renvoyer la valeur fixe lorsque
primaryAxisSizingMode === "FIXED". Une approximation grossière suffit pour détecter le chevauchement de lignes.
Exécutez chaque chemin de répartition, chaque point de rupture et chaque configuration de police, pas seulement le chemin heureux :
Inter seulement · Inter + mono · les trois · Inter Regular seulement
desktop · mobile dark · light
Polices : chargez ce que vous demanderez, et résolvez ce que vous obtenez
Affecter une police non chargée lève une exception. Deux pièges derrière cela :
- Vous devez précharger chaque poids que le code demande. Charger Regular, Medium et Bold puis demander Semi Bold est un crash qui attend la carte qui l'utilise.
- Le nom du style diffère selon la famille. Inter dit
"Semi Bold"; JetBrains Mono dit"SemiBold". Demander le bon poids avec la mauvaise orthographe lève toujours une exception.
Sondez chaque poids séparément, enregistrez ce qui a réellement été chargé, et résolvez chaque demande via une chaîne de repli plutôt que de supposer :
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" };
}
Ne préchargez jamais strictement dans le point d'entrée avec Promise.all — un poids
manquant là tue une build que le résolveur aurait gérée. Signalez les familles
auxquelles vous avez résolu dans le toast de fermeture.
Un plugin, une tâche
Résistez à un commutateur de répartition à l'intérieur d'un seul plugin — sur le type d'éditeur, le nom de page ou la sélection. Un commutateur basé sur le nom de page signifie que l'utilisateur doit effectuer un rituel avant que la chose fonctionne, et quand elle fait silencieusement la mauvaise chose, il n'y a rien à lire.
Deux manifestes avec des valeurs id et name différentes ne coûtent rien et suppriment toute
cette classe de problèmes. Figma réutilise également un enregistrement mis en cache lorsqu'un
manifeste réimporté porte un id qu'il connaît déjà, donc changer l'id est la façon dont
vous forcez un chargement propre.
Lire les commentaires en retour
Le MCP expose get_metadata, get_design_context, get_screenshot,
get_figjam, use_figma — aucun d'eux ne lit les commentaires, et l'API Plugin
n'a pas non plus d'accès aux commentaires. Les commentaires vivent dans l'API REST.
Utilisez tools/figma_comments.py dans ce dossier de skill (Python 3, bibliothèque standard) :
T=~/.claude/skills/figma-build/tools/figma_comments.py
python3 $T check <file-url> # OK : le jeton lit les commentaires (N sur file KEY)
python3 $T fetch <file-url> # fils de discussion ouverts au format JSON, enregistrés et résolus filtrés
python3 $T mark <file-url> ID... --change "ce qui a changé"
python3 $T reply <file-url> ROOT_ID "Fait : logo 20% plus grand" # nécessite le champ d'écriture
Il lit le jeton depuis ~/.figma-token et ne l'imprime jamais. Exécutez-le depuis le dossier de
travail pour que le registre y atterrisse, ou définissez FIGMA_LEDGER. Il n'y a pas de commande de suppression.
S'il signale une erreur de certificat SSL, le Python n'a pas de certificats racine :
exécutez /usr/bin/python3 à la place, ou pip install certifi.
Portée : Commentaires sur le fichier → Lecture. Stockez le jeton dans un fichier (~/.figma-token,
chmod 600), jamais dans la conversation. Si la personne ne peut pas exécuter une commande shell,
écrivez le fichier avec un espace réservé et open -a TextEdit pour qu'elle puisse le coller.
/v1/me renvoie 403 sur un jeton avec la portée commentaires. C'est attendu, pas un
jeton cassé. /v1/files/:key/nodes renvoie également 403 sans file_content:read.
Chaque commentaire porte client_meta :
{ "node_id": "2:95", "node_offset": { "x": 279, "y": 1257 } }
Mappez-le à un bloc en extrayant get_metadata et en trouvant la frame enfant dont
la plage y contient le décalage. Les fils de discussion reviennent à plat — regroupez par parent_id, et
les réponses contiennent souvent l'instruction réelle tandis que la racine n'est qu'un pointeur.
Les commentaires sont par fichier. Déplacer du contenu vers un nouveau fichier les laisse derrière.
La boucle de relecture, lorsqu'on vous demande de « lire mes commentaires »
- Récupérez avec
figma_comments.py fetch, qui regroupe les commentaires en fils de discussion. Ignorez ceux qui sont résolus (resolved_atdéfini) et tous les identifiants déjà dans le registre traité (ci-dessous). Les anciens commentaires restent dans le fichier jusqu'à ce qu'ils soient résolus, donc sans ces deux filtres, chaque relecture, et chaque conversation ultérieure, réapplique les retours du tour précédent. - Mappez chaque fil de discussion à sa frame via
client_meta, comme ci-dessus. - Confirmez avant de dessiner : listez chaque commentaire, la frame à laquelle il correspond, et le changement prévu, dans un court tableau. Un commentaire épinglé sur un canvas vide, ou ambigu, donne lieu à une question plutôt qu'à une supposition.
- Appliquez les modifications sur place : modifiez les propriétés des nœuds existants, en conservant leurs identifiants. Ne supprimez jamais et ne redessinez pas une frame commentée, car cela orphelinerait ses commentaires. Via le MCP (
use_figma), le canvas se met à jour directement. Via un plugin de développement, modifiez son code sur le disque, incrémentez le tampon de build, et demandez à la personne d'exécuter à nouveau le plugin. - Vérifiez avec
get_metadataet une capture d'écran (voir Mesurez, ne regardez pas). Sur la voie du plugin, vérifiez que le toast de fermeture montre le nouveau tampon avant de faire confiance au canvas. - Rapportez commentaire par commentaire ce qui a changé. Si le jeton a le champ d'écriture et que la personne le souhaite, répondez dans chaque fil de discussion.
- Enregistrez chaque identifiant de commentaire traité (racines de fils et réponses) avec
figma_comments.py markavant la fin du tour, y compris ceux auxquels vous avez seulement répondu par une question. - Ne supprimez jamais les commentaires, même lorsqu'on vous demande de nettoyer. Ils sont l'historique de la relecture. Les anciens retours restent hors des tours suivants grâce au registre, pas en les supprimant. Si un tour a besoin d'être effacé dans Figma, la personne résout les commentaires.
Le registre traité. Un fichier JSON à côté du travail : figma/comments-handled.json dans un dossier de campagne, sinon figma-comments-handled.json dans le répertoire de travail. Lisez-le au début de chaque relecture, y compris dans une nouvelle conversation.
{ "<file_key>": { "<comment_id>": { "handled_at": "2026-11-03T14:05:00Z", "change": "logo 20% plus grand sur 1080x1920" } } }
Il n'y a pas de point d'API pour résoudre un commentaire, donc la résolution reste avec la personne.
Quand demander une réimportation (voie du plugin uniquement) : lorsque le toast montre un tampon obsolète, ou « replaced 0 » alors que des frames sont visibles. Demandez-leur de supprimer le plugin, d'importer à nouveau son manifest.json et de rouvrir l'onglet. Si cela échoue, changez l'id du manifest (voir Un plugin, une tâche).
Captures d'écran à grande échelle
Une capture par page plus des recadrages locaux est plus efficace qu'un appel par bloc. Le y/hauteur du bloc
provient de get_metadata, maxDimension au-dessus ou à la hauteur de la frame donne du 1:1,
et les recadrages s'alignent exactement.