entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

Figma でアイコンを更新したら GitLab に勝手に MR が作られるやつ

こんにちは、@f_subal です。普段はおもに pixivFACTORY のフロントエンドを見ています。最近は社内のデザインシステム整備の仕事もやっており、今回はそちらで作っているアイコンライブラリの話をします。

SVG アイコンを社内 npm で配る

みなさん、プロダクト内で利用するアイコンをどのように管理していますか?

サイズ別のアイコンの一覧

大抵の場合は元になる .svg ファイルが存在し、それを最終的に React コンポーネントで読んだり、あるいは昔ながらのアイコンフォントを生成したりして使っているでしょう。

ピクシブではこれまで各プロダクトがそれぞれの方法でアイコンを生成していました。あるプロジェクトは svg スプライトを生成して <use> タグで読み、またあるプロジェクトは svgr を使い、これまたあるプロジェクトでは woff を生成する npm スクリプトを持ったりしています。

実装方法はともかく、ソースとなる svg ファイル自体を各プロダクトのリポジトリが持つのはやっぱりだるいです。デザイナとしても一元管理しにくいですし、ここは一つ共通ライブラリとして配られていて欲しいと思うのが人情でしょう。

ピクシブでは最近社内でデザインシステムの整備を進めており、その一環としてアイコンライブラリをプライベートな npm レジストリで配ることにしました。

Figma の更新をコードベースに反映する

ピクシブではプロダクトデザインに主に Figma を使用しています。ピクシブの全プロダクトで利用しているアイコンも、Figma の専用ファイル上に整理されつつあります1

Figma 上のアイコン一覧

現状欲しいアイコンがある場合、ここから svg でエクスポートして各リポジトリにコミットなどが行われるわけですが、そうではなくこれを Single source of Truth とするような npm パッケージが一個あれば良いわけです。

ライブラリ側は最新に保たれていてほしいので、デザイナが Figma を更新したら勝手に Merge Request が送られてきて、良さそうならそのまま publish したい。デザインシステムが現状プロダクトと掛け持ちで開発されていることを考えると、可能な限り反映は自動化したい。要はアイコン版 dependabot が欲しいわけですね。

一応先行事例として GitHub の octicons がかつて Figma の更新を Github Actions で反映する仕組みを使っていたようです( https://github.com/primer/figma-action2。弊社は主に GitLab を利用しているので、 PR 作成部分だけうまく変えてやれればやっていけそうです。今回大いに参考にしました。

icons-cli を設計する

ピクシブのデザインシステムは Lerna + Yarn workspace を用いた monorepo 構成です。アイコン以外にもいくつかのコンポーネントライブラリや、Tailwind CSS の設定ファイルをパッケージとして擁する作りになっています。

ここにアイコンとは別にアイコン反映スクリプトのパッケージを作成します(以下、アイコンライブラリそのものを icons、反映ツールを icons-cli と呼びます)。

- /packages
  - /icons
    - /src
      - /16
        - 〇〇.svg
      - /24
        - 〇〇.svg
  - /icons-cli

icons-cli は yargs 製の CLI ツールで、実行すると icons の src ディレクトリに最新の svg ファイルを保存したり、その変更を GitLab API 経由で Merge Request にする機能を持ちます。ちょっと monorepo そのものに密結合ではありますが、最終的にこれを CI で定期実行することを目指します。

icons-cli のヘルプ

Figma API を叩いて svg を取り出す

まず、Figma の特定のファイルから必要な SVG を読んでファイルに書き出す処理が必要です。だいたい先述の primer/figma-action を真似すれば良いのですが少し詳細を解説しましょう。


Figma のファイルは node と呼ばれる子を多数持った木構造をしています。Figma の UI 上ではページやグループやコンポーネントなど粒度の異なる概念が色々出てきますが、Figma API ではこれらはすべて node の一種です( node.type を見ることでその内容がページなのかコンポーネントなのかを確認できます )。

https://www.figma.com/developers/api

なぜそんな構造をしているのかはさておき3、問題は Figma API には特定の node とその子要素をとるエンドポイントというのが存在しないことです。file は id を指定して取ることができますが、この file の1ページ目だけ欲しいといったユースケースでは、file を取得完了後にこちら側でページの中身を取り出す必要があります。

今回は「Figma 上の特定の id の file の特定のページにある、特定の命名規則にそったコンポーネントを取り出す」といった要件になります。この場合、id を指定して file を取得した後は子 node を再帰的に traverse していき、目的の node を拾っていけば良いでしょう。

次に、当該コンポーネントを画像に書き出します。Figma には特定ファイル内の node の id 一覧を渡すと、それを一斉に画像書き出ししてくれるエンドポイントがあります。さっき取り出した対象の node たちの id でリクエストすると画像の URL が得られるので、あとはそいつらをダウンロードし、ローカルに .svg ファイルとして書いていけばよいです。

GitLab API でリポジトリに反映

さて、ファイルが得られたら今度はそれをリポジトリにコミットし、MR にする必要があります。

リポジトリにファイルをコミットするにはその場で git commit を叩いても良いのですが、GitLab API 経由でコミットすることも可能です。どのみち MR の作成には API を使うわけですから、コミットも API 経由で行うことにします(GraphQL API もありますが今回は REST の方を使います)。

https://docs.gitlab.com/ee/api/commits.html#create-a-commit-with-multiple-files-and-actions

GitLab のコミット API はプロジェクト ID、ブランチ名(とデフォルトブランチ)、コミットメッセージ、変更内容を json で表現したものを取ります。問題は「変更内容を json で表現したもの」で、これを組み立てるのに多少面倒が必要になります。

今回使ったのは @gitbeaker/node ですが、変更内容は次のようなオブジェクトの配列で表現する必要があります(以下は gitbeaker の型定義より抜粋)。

export interface CommitAction {
    /** The action to perform */
    action: 'create' | 'delete' | 'move' | 'update';
    /** Full path to the file. Ex. lib/class.rb */
    filePath: string;
    /** Original full path to the file being moved.Ex.lib / class1.rb */
    previousPath?: string;
    /** File content, required for all except delete. Optional for move */
    content?: string;
    /** text or base64. text is default. */
    encoding?: string;
    /** Last known file commit id. Will be only considered in update, move and delete actions. */
    lastCommitId?: string;
}

最低限必要なのはファイル名と、それが「新規追加」なのか「更新」なのかを示す文字列、後はその内容ということになります(ほかは省略してもなんとかなります)。

当初、私は適当にファイルを与えたら勝手に GitLab 側で new file か modified か(あるいは変更がなかったことも含めて)判定してくれるんじゃないかと期待していました。が実際はそんなことはなく、矛盾した diff にならないよう API の利用側でうまく判定する必要があります。


ここで注意すべき点は、今われわれの反映スクリプトは GitLab CI 上の Docker マシンで動くことを想定していることです。コンテナ下ですから当然実行ごとに環境が独立していますし、git clone も実行ごとに行われます。

これはつまり、ファイルシステム上の作成日時や更新日時( stat コマンドによって得られるそれ )は基本当てにならないということです4。うっかり最初 Node の fs.statSync() と最終コミット時刻を比較してしまいハマりました。

こういう時にファイルに更新があったことを検知したければ、やはり git の差分管理に頼るのが便利です。git status には --porcelain というオプションがあり、普段我々が見ているのとは違う機械判読可能なフォーマットで git status を表示してくれます。これをパースして追加や変更があったファイルの一覧を得ればよいというわけです( 今回の反映スクリプトで唯一 git コマンドに直接依存している箇所がここです )。

$ git status --porcelain
 M packages/icons/src/16/Modified.svg
?? packages/icons/src/16/NewFile.svg

https://git-scm.com/docs/git-status#_porcelain_format_version_1

const gitStatus = Object.fromEntries(
  // execp は child_process/exec を promisify したものです
  (await execp(`git status --porcelain`))
    .split('\n')
    .map(
      s =>
        [
          s.slice(3),
          s.startsWith(' M') ? 'modified' : s.startsWith('??') ? 'untracked' : null
        ] as const
    )
)

// { “packages/icons/src/16/〇〇.svg”: “modified”, “packages/icons/src/24/〇〇.svg”: “untracked”, ... }

GitLab CI で稼働させる

ここまで来たらあとは GitLab CI で icons-cli を定期実行するスケジュールが設定できれば OK です。スケジュールの設定画面でアクセストークンなども入れてあげれば完成です。

MR が Slack に通知されて喜ぶ様子

GitLab で MR ができたときなどに Slack へ通知しているのですが、実際デザインシステム開発チームは bot の出した変更をレビューして好きな時にマージと publish をすれば良くなりました。便利ですね。

現状の課題としては、同じ内容で MR を作ったことを bot が覚えてない実装になっていることです。今は一日一回のペースで定期実行しているのでそんなに問題ないですが、これがもし一時間に一回だった場合、一時間以内にマージしないともう一回同じ MR が来ることになります(今でも一日放置したらもう一回来るんですが、そのくらいの圧ならむしろ丁度いいかと思って変えてません5 )。

ただ、それを記録するための DB を持つとかはしんどいのでもう少し楽な方法で回避したいですね。bot による MR が 1 個でもあったら中断でもいいですし、もう少し気を利かせるなら diff の内容からダイジェスト値を作って description に含め、同じ description の MR があったら中断とかをやっても良いかもしれません。

まとめ

SVG アイコンを社内ライブラリとして運用していくにあたり、Figma から GitLab への反映を自動化しました。これにより、.svg ファイルを手動で Figma から反映する作業が不要になりました。

現状アイコンライブラリはただ .svg ファイルが入っているだけの npm パッケージになっており、いささか各プロダクトからは使いにくい状況です。 今後はこの svg を元にしたコンポーネント実装も配っていくことで社内のプロダクトに広げていければと思います。

ピクシブ株式会社では、デザインシステムを強くし、広げてくれるフロントエンドエンジニアを募集しています!

hrmos.co


  1. ピクシブのアイコンシステムには 16 / 24 / 32 / Inline というサイズ区分があります。アイコンにはそれぞれ想定するサイズがあり、中途半端な倍率で表示すると hidpi ではない環境でアンチエイリアスがかかり、ピクセルパーフェクトではなくなります。これを防ぐために SVG アイコンでもサイズを複数用意する運用をしています。
  2. 厳密には octicon の事例は我々とはアプローチが異なります。octicon では master ブランチに push した際に CI でアセットを figma から取っており、それによって得られた .svg はバージョン管理の対象になりません。実際その辺りで面倒になったためか、octicon は現在 svg をリポジトリで管理する仕組みに変わっています。https://github.com/primer/octicons/pull/374
  3. これは全くの想像ですが、Figma はコンポーネントもグループも矩形も、粒度に関係なく画像として export できるという特徴があります。それを実現するためにはあらゆる描画対象を node という一つの概念で表す必要があったんだろうと勝手に思っています(実際その後に出てくる画像書き出し API は node の id 一覧を受け取っています)。
  4. この場合、ファイルの mtime はコミットした時間ではなく git clone した時間に一致します。git はファイルの更新日時を管理しているわけではないので、最終コミット日時とファイルの編集日時を比較することには実は意味がありません。
  5. GitLab CI の crontab で平日のみの設定にしてますから、変更を放置したまま土日をまたいでも MR が 3 つに増えたりはしません。3連休をまたいだ時は来るんですが、まぁそれは放置するのが悪いです。
20191219021843
f_subal
2016年新卒入社。sensei by pixiv の開発や pixiv 本体投稿画面リニューアルなどを経て、pixivFACTORY のエンジニアをしている。百合と TypeScript が好き。