KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

Figma からアイコンの画像を生成して GitHub の PR を作る Widget の作り方

はじめに

こんにちは、Pocket Musubi エンジニアの関(@sekikazu01)と申します。

「あ〜アイコン大量に増えた時逐一画像を書き出して Icon コンポーネントに反映させるのめんどくせ〜〜〜」

そんな風に思った事はないでしょうか。私は思いました。 ので Figma のアイコンコンポーネントからコードに反映するまでのパイプラインを作りましたので、そのコードを公開していきます。

この記事はアイコンの生成の話ですが、一回作っておくと他にも画像だったりコンポーネントだったり諸々の生成パイプラインを作る時にも役立つと思います。

また、敬意を表すべくこのパイプラインを作るにあたって参考にさせていただいた先人の記事を紹介しておきます。

やっていることは 9 割方同じなのですが、デザイナーがアップデートした時に Figma から変更通知できるような Widget を作ってみたのが、この記事でオリジナリティがあるところです。

作ったものは大きく分けて次の3つがあります。

  1. Figma API でアイコンの画像データを取得して書き込むスクリプトを作る
  2. それを GitHub Actions で実行できるようにする
  3. 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>

これをローカルで作って設置すれば準備万端です。

スクリーンショット 2022-07-23 18 05 40

あとはデザイナー氏に「アイコン増えたらこれポチッと推してもらえると助かる」と伝えるだけです。

おわりに

以上、Figma のアイコンをコードにも反映する仕組みの作り方を紹介しました。 冒頭にも述べた通り、他にも Figma から自動でアプデできたら嬉しいな〜と思うのはちょいちょいあるので一度作っておくといろんなものに応用が効くと思います。

あとこれは副次的に思った事ですが、こういう仕組みがあるとデザインを規則立てて作ることにもインセンティブが生まれるので、(面倒な制約にも感じるかもしれませんが)長期的にそういった面でもプラスの価値を生みそうだなと感じました。

それでは、皆様も  Figma と GitHub を組み合わせて色々遊んでみてください!