🎨

FigmaのSVGアイコンからReactコンポーネントを作成しPRを出す

2021/12/22に公開

はじめに

Figmaでデザインのマスターとして、独自のアイコンを管理することはよくあると思います。しかし、それをもとにReactでの実装をする際に、いちいちFigmaを操作して出力し、コンポーネントしていくのは地味に面倒です。
また、よく発生した問題点として、デザイナーがfigma上でアイコンを編集していたことが実装者に伝わらずに、アウトプットが予想と違うものになってしまうことが多々ありました。

そこで、Figma APIを使用しコマンド一つで必要なものを生成するようにします。

手順

流れは以下のようになります。

  1. Figmaのファイルを整理し、アイコンをコンポーネント化する
  2. Figma APIをたたき、svgファイルを取得する
  3. svgをもとに.tsx化する
  4. 以上のコマンドを発火するactionからPRを出す

が、ちょうどこれを作成し、ブログネタにもなるぞ!!
と喜んだその日に、こちらのブログがTLに流れてきました。。。。。
https://tjmschk.hatenablog.com/entry/figma-svg-to-github

はい、、、、
scriptとか自分の載せてもただのパクリにしかならないレベルで同じです、、、、
逆に関数分割とかしてなくて、汚いです、、、、

figma APIを叩くところまでは、scriptの内容までほぼ同じなので、こちらをご覧いただければ十分かと思います。
(figma側の整理で、pathをoutline化しないとapi経由適切に取得できてなかった、など発生したので注意が必要そうです)

3. svgをもとに.tsx化する

ということで、いきなり飛ばして、svgをコンポーネント化します。
有名どころですが、SVGRを使用しました。

今回こだわったところとして、ただReactコンポーネント化するのではなく、MUI Iconsのように、あらかじめ決められたサイズのみを使用できる制限をつけたコンポーネントにしていきます。

svgrのコマンドはこのようになります。

npx @svgr/cli -d src assets --typescript --jsx-runtime automatic --replace-attr-values '#30313B=currentColor' --icon --template template.js

コマンドの意味としては、

  • もちろん.tsxファイル以外見たくない人生なので、 --typescript
  • React 17以上にしか対応しなくて良いので --jsx-runtime automatic
  • 色を使用時に変更できるようにfigmaで使用している色をcurrentColorに変更 --replace-attr-values '#30313B=currentColor'
  • templateからサイズをfontSizeでいじりたいので --icon
  • 全てのファイルでスタイリング上書きしたいので、カスタムのtemplateを使うので --template template.js

という内容です。

そしてtemplate.jsはこのようになりました。

template.js
const template = (variables, { tpl }) => {
  const componentName = variables.componentName.replace('Svg', '');
  return tpl`
        import { css } from '@emotion/react';
        ${variables.imports};

        const SvgIcon = (${variables.props}) => ${variables.jsx}
        
        interface Props extends SVGProps<SVGSVGElement> {
            size?: 'small' | 'medium' | 'large' 
        }
        
        const sizes = {
            small: 20,
            medium: 24,
            large: 36,
        };

        const svgCss = (size: Props["size"]) =>
            css({
                width: '1em',
                height: '1em',
                color: 'currentColor',
                userSelect: 'none',
                fontSize: sizes[size || 'medium'],
                strokeWidth: '1.5px',
                display: 'inline-block',
                flexShrink: 0,
            }); 
            
        const ${componentName} = (props: Props) => (
            <SvgIcon focusable="false" css={svgCss(props.size)} {...props} />
        );
        
        export default ${componentName};
    `;
};

module.exports = template;

これを通すことによって例えば、
このようなsvgが

ArrowDown.svg
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M12 4V20" stroke="#30313B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
  <path d="M18 14L12 20L6 14" stroke="#30313B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

このようなtsxファイルが生成されます。

ArrowDown.tsx
import { css } from '@emotion/react';
import { SVGProps } from 'react';

const SvgIcon = (props: SVGProps<SVGSVGElement>) => (
  <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
    <path d="M12 4v16M18 14l-6 6-6-6" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
  </svg>
);

interface Props extends SVGProps<SVGSVGElement> {
  size?: 'small' | 'medium' | 'large';
}
const sizes = {
  small: 20,
  medium: 24,
  large: 36,
};

const svgCss = (size: Props['size']) =>
  css({
    width: '1em',
    height: '1em',
    color: 'currentColor',
    userSelect: 'none',
    fontSize: sizes[size || 'medium'],
    strokeWidth: '1.5px',
    display: 'inline-block',
    flexShrink: 0,
  });

const ArrowDownStroke = (props: Props) => <SvgIcon focusable="false" css={svgCss(props.size)} {...props} />;

export default ArrowDownStroke;

生成されるjsxを一度そのままでコンポーネント化して、そこに上書きするようにtemplateを作ることでより自由にtemplate表現ができます。

ただし、storkeWidthに関しては、cssで上書きできる余地を残すために別のコマンドで一括で消したりしています。

find ./src -name '*.tsx' -exec sed -i '/strokeWidth={1.5}/d' {} \\;

以上で好きなようにコンポーネントを作ることができました。

3. github actionからPRを出す

さきにあげた記事のように定期実行なども考えましたが、アイコンに変更するのはある一時的に変更頻度が多く、普段ほぼ変更しないことが今までの傾向としてあったので、ボタン一つでPRが作成されるactionで落ち着きました。
master pushにしなかったのは、デザインに変更があったことを、PRの存在を通してエンジニアに明確に伝えるためです。

update-icon.yml
name: Update Icon

on: [workflow_dispatch]

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: '14.x'
      - name: Install
        run: npm ci
	
      - name: Update Icon
        run: FIGMA_TOKEN=${{ secrets.FIGMA_TOKEN }} FIGMA_FILE_KEY=${{ secrets.FIGMA_FILE_KEY }} npm run build:asset
	
      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v3
        with:
          token: ${{ secrets.REPO_ACCESS_TOKEN }}
          commit-message: 'feat: update icons'
          committer: GitHub <noreply@github.com>
          author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
          branch: feat/update-icons
          branch-suffix: timestamp
          delete-branch: true
          title: 'feat: update icons'
          body: Icons has been updated

このようなymlを置いておけば、
デザイナーに追加でお願いするのはfigma変更後に起動するボタンを押すことだけで、

このようにPRが生成され、

あとは、普段通りのレビュープロセスを通せば良いだけです🎉
(figmaから何かイベント飛ばしてactionできれば理想と思ったがアイデアがなく、、、何かいい方法ありますかね🤔)

おわりに

以上、ネタが被ったというネタによる自分の中のモチベが高いうちに、ざっと今回やれたことを実際のコードを交えてまとめてみました。

ちなみに、以前書いた
この記事の手法を使って、storybookの自動生成をし、それをreg-suitによるVRTに通すことで、生成したPR単位でのstorybook確認と他のアイコンに影響がないことも担保しています。
https://zenn.dev/kodai/articles/10eb1308e73574

引き続き、DesignOpsと呼ばれるようなkaizen色々試して行きたいと思っています。

Discussion