はじめに
こんにちは、Pocket Musubi エンジニアの関(@sekikazu01)と申します。
「あ〜アイコン大量に増えた時逐一画像を書き出して Icon コンポーネントに反映させるのめんどくせ〜〜〜」
そんな風に思った事はないでしょうか。私は思いました。 ので Figma のアイコンコンポーネントからコードに反映するまでのパイプラインを作りましたので、そのコードを公開していきます。
この記事はアイコンの生成の話ですが、一回作っておくと他にも画像だったりコンポーネントだったり諸々の生成パイプラインを作る時にも役立つと思います。
また、敬意を表すべくこのパイプラインを作るにあたって参考にさせていただいた先人の記事を紹介しておきます。
やっていることは 9 割方同じなのですが、デザイナーがアップデートした時に Figma から変更通知できるような Widget を作ってみたのが、この記事でオリジナリティがあるところです。
作ったものは大きく分けて次の3つがあります。
- Figma API でアイコンの画像データを取得して書き込むスクリプトを作る
- それを GitHub Actions で実行できるようにする
- Figma の Widget で Figma のファイルから上記アクションを Webhook で実行できるようにする
Figma API でアイコンの画像データを取得して書き込むスクリプトを作る
まずは Figma API でアイコンコンポーネントのデータを取ってきて、 /public/images/icons/
に SVG として書き出す & アイコンの名前の配列を書き出すスクリプトを書きます。
アイコンの書き方だったり、Figma からアイコンコンポーネントを取ってくる際のロジックはご自身のチームの開発スタイルや Figma の命名規則に合わせて適宜書き換えてください。
import * as fs from 'fs'; import fetch from 'node-fetch'; const baseUrl = 'https://api.figma.com/v1/'; const fileId = '秘密だよ'; const tokenArg = process.argv[2]; if (!tokenArg) { console.log(`Figma のトークンをセットしてください! 例: npm run import-icons-from-figma FIGMA_TOKEN=xxxxxxxx`); process.exit(); } const figmaToken = tokenArg.split('FIGMA_TOKEN=')[1]; const fetchFileData = async () => { const res = await fetch(`${baseUrl}files/${fileId}`, { headers: { 'X-Figma-Token': figmaToken, }, }); return await res.json(); }; const fetchIconImageUrls = async (iconComponentNodeIds) => { const res = await fetch( `${baseUrl}images/${fileId}?ids=${iconComponentNodeIds.join()}&format=svg`, { headers: { 'X-Figma-Token': figmaToken, }, } ); return await res.json(); }; const extractIconImages = (iconComponentNodeIds, fileData, iconImgUrls) => { return iconComponentNodeIds.map((nodeId) => { const node = fileData.components[nodeId]; const { name } = node; const link = iconImgUrls.images[nodeId]; // Figma での名前はこんな感じ: icon / articles24 // 先頭の "icon / " の部分 & 数字の部分を取り除く return { name: name .replace('icon / ', '') .replace(/[0-9]/g, '') .split('/') .map((s) => s.trim()) .join('-') .split(' ') .join('-'), link, }; }); }; const writeToIconNames = (iconNames) => { const fileContent = `export const iconNames = [${iconNames .map((name) => `"${name}"`) .join(",")}] as const; export type iconTypes = typeof iconNames[number]; ${iconNames .map( (name) => `import ${name .split("-") .map((s) => s.toUpperCase()) .join("")} from '../../../public/images/icons/${name}.svg';` ) .join("\n")} export const iconMap: { [key in iconTypes]: React.ComponentClass } = { ${iconNames .map( (name) => `'${name}': ${name .split("-") .map((s) => s.toUpperCase()) .join("")}` ) .join(",\n ")} }; `; fs.writeFileSync(`./src/components/Icon/IconNames.ts`, fileContent); }; const run = async () => { // まずはファイルを丸ごと取得する const fileData = await fetchFileData(); const iconComponentNodeIds = Object.keys(fileData.components).filter( (nodeId) => { const node = fileData.components[nodeId]; return node.name.includes('icon /'); } ); // Icon コンポーネントの画像のリンク集を取得 const iconImgUrls = await fetchIconImageUrls(iconComponentNodeIds); // ファイル名をコンポーネントの名前から作る & 画像のリンクとセットにしたオブジェクトを作る const iconImages = extractIconImages( iconComponentNodeIds, fileData, iconImgUrls ); // 画像を Figma の URL から取りつつ書き込み iconImages.forEach(async (image) => { const res = await fetch(image.link); const data = await res.text(); fs.writeFileSync(`./public/images/icons/${image.name}.svg`, data); }); // アイコンの名前の配列をファイルに書き込み const iconNames = iconImages.map((image) => image.name); writeToIconNames(iconNames); }; run();
これを実行するためのコマンドを package.json に追記しておきます。
{ "scripts": { "import-icons-from-figma": "node scripts/import-icon-from-figma.mjs" } }
実行する時はこんな感じで引数に Figma API のトークンを渡します。
npm run import-icons-from-figma FIGMA_TOKEN=xxxxxxxxxxxxxx
ちなみにこうやって生成された画像とアイコンの名前の配列はこんな感じの Icon コンポーネントに活用されます。
import { styled } from "@/styles/stitches.config"; import { iconMap, iconTypes } from "./IconNames"; type Size = "large" | "medium" | "small" | "extra-small"; type IconStyleType = "primary" | "black" | "descrutive" | "white"; type Props = { type: iconTypes; fill?: IconStyleType; accessibilityLabel?: string; size?: Size; }; export const Icon: React.FC<Props> = ({ type, accessibilityLabel, fill = "primary", size = "medium", }) => { const IconComponent = iconMap[type]; return ( <IconStyle aria-label={accessibilityLabel} size={size} fill={fill}> <IconComponent /> </IconStyle> ); }; const IconStyle = styled("div", { ... });
GitHub Actions で実行できるようにする
次に先ほど作ったスクリプトを実行する GitHub actions を作ります。
以下を .github/workflows
の中に入れておきます。
今回は PR を生成していますが、「いや、確認いらん!」という場合は直 commit にしてもいいかもしれません。
name: Update Icon on: workflow_dispatch: repository_dispatch: types: - update-icons jobs: update-icon: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0 - uses: actions/setup-node@v2 with: node-version: '16.x' - name: Install run: npm ci - name: Update Icon run: npm run import-icons-from-figma FIGMA_TOKEN=${{ secrets.FIGMA_TOKEN }} - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: token: ${{ secrets.REPO_ACCESS_TOKEN }} commit-message: 'update icons' committer: GitHub <noreply@github.com> author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> branch: feature/update-icons branch-suffix: timestamp delete-branch: true title: 'アイコンのアップデート' body: Icons has been updated
workflow_dispatch
が GitHub のサイト上から実行するのに必要で、repository_dispatch
が後程 Webhook で実行するのに必要です。
ちなみに secrets の設定の仕方が分からなければこちら(Encrypted secrets)を参考にしてください。
Figma の Widget で Figma のファイルから上記アクションを Webhook で実行できるようにする
最後に Figma Widget を作って、デザイナーがアイコンを増やした時にポチって押してもらえば反映できる仕組みにします。
作った Widget のコードは下記レポジトリに入れておいたので clone いただけるとサクッと作れるかなと思います。
やってることは至極単純で先ほど作った Actions を Webhook で実行しているだけです。
<script> window.onmessage = (event) => { if (event.data.pluginMessage.message === 'sync-github') { const invokeWebhookUrl = 'https://api.github.com/repos/{YOUR_ORG_NAME}/{YOUR_REPO_NAME}/dispatches'; const githubPAT = ''; // set your GitHub access token fetch(invokeWebhookUrl, { method: 'POST', body: `{"event_type": "your-webhook-title"}`, headers: { Authorization: 'token ' + githubPAT, }, }).then(() => { parent.postMessage({ pluginMessage: { type: 'close-plugin' } }, '*'); }); } }; </script>
これをローカルで作って設置すれば準備万端です。
あとはデザイナー氏に「アイコン増えたらこれポチッと推してもらえると助かる」と伝えるだけです。
おわりに
以上、Figma のアイコンをコードにも反映する仕組みの作り方を紹介しました。 冒頭にも述べた通り、他にも Figma から自動でアプデできたら嬉しいな〜と思うのはちょいちょいあるので一度作っておくといろんなものに応用が効くと思います。
あとこれは副次的に思った事ですが、こういう仕組みがあるとデザインを規則立てて作ることにもインセンティブが生まれるので、(面倒な制約にも感じるかもしれませんが)長期的にそういった面でもプラスの価値を生みそうだなと感じました。
それでは、皆様も Figma と GitHub を組み合わせて色々遊んでみてください!