⚒️

コードの自動生成はすべて明示的に、そして Git にコミットする運用

2023/12/08に公開

株式会社マネーフォワード福岡拠点でフロントエンドエンジニアをしている樫福です。
この記事は Money Forward Fukuoka Advent Calendar 2023 の8日目の記事として投稿しています。

7日目の昨日は廣池さんの Next.js の App routerで環境変数を利用するときにハマったことでした。

はじめに

何らかの情報を使ってコードを自動生成することがあります。たとえば、 GraphQL Code Generator は GraphQL のスキーマファイルをもとに TypeScript の型やユーティリティ関数を生成します。
また、 SVGR のようにファイル自体は吐き出さないものの SVG ファイルを React コンポーネントに変換してから利用しています。

このような、あるデータから変換されて作られたコードは、明示的に生成されなかったり .gitignore されてコミット対象にから外されたりすることがあります。
私が現在所属しているチームでは、これらのコードをすべて明示的に生成し Git にコミットしています。この記事では、このような運用を約半年続けて感じたことを共有します。

明示的な生成

ここで "明示的" というのは、生成したコードを実装者の手に取れる形で生成することを指しています。逆に、生成したコードをビルドプロセスや dev 環境での実行時に内部的に保持し実装者の目に触れない状態で使うことを、 "暗黙的" であると呼ぶことにします。

やったこと

ここでは、 SVGR の設定を例にやったことを紹介します。
前述した通り、 SVGR は SVG ファイルを React コンポーネントに変換するライブラリです。 Next.js での使い方を見てみると next.config.js に @svgr/webpack というライブラリを指定しています。変換後のファイルが暗黙的に生成しています。

生成されたコンポーネントを利用する場合でも、 import 文はもとの SVG ファイルのパスを指定しればよいです。

import PlusIcon from './icons/plus.svg'

const AddButton = () => {
  return (
    <button>
      <PlusIcon />
    </button>
  );
}

私たちのプロジェクトでは、 SVGR の CLI ライブラリを使って明示的に生成しています。 package.json に "generate:svg" のような変換コマンドを登録しておいて、必要に応じて生成します。

生成されたコンポーネントを利用する場合は、 import 文では生成後の SVG ファイルのパスを指定することになります[1]

import { PlusIcon } from './icons/generated/Plus.tsx';

const AddButton = () => {
  return (
    <button>
      <PlusIcon />
    </button>
  );
}

✅ よかった点

実行環境に依存せずコードを利用できる

たとえば、 SVGR のサイトではコードを暗黙的に生成する環境で Jest を使ってコードのテストする場合には SVG ファイルをモックで置き換える方法が紹介されています
CLI ライブラリを使ってコンポーネントを生成する方法だと、すべての環境で実装を共通にできます。

暗黙的に生成したコードを利用する場合、どうしても実行環境ごとにコード生成する手段を作る、もしくはモックを利用する必要があります。実行環境のバリエーションが増えれば増えるほど、セットアップや保守の手間が大きくなります。
また、モックを使わなくて済む点も、テストの信頼性の観点で嬉しいです。

🤔 気になる点

データの変更後に再生成を忘れることがある

データの変更後に再生成を忘れると、コードが更新されず古い情報のままになってしまいます。そのまま開発を進めてしまったり、そのままリリースしてしまったりするリスクがあります。
ライブラリによっては watch モードを使って元データを更新するたびに再生成することも可能ですが、こちらも watch モードを起動すること自体を忘れると意味がないので根本的な解決にはならないです。
後述するような CI を用意して、再生成を忘れてても気付けるような仕組みを作りました。

ライブラリのアップデートに影響で差分が生じることがある

ライブラリがアップデートされると、生成されるコードにも変更が加わる可能性があります。すぐに気づければよいですが、放置してしまうと意図しないタイミングで差分が発生してしまいます。
こちらも後述する CI によって気付けるようにしていて、大きな問題だとは感じていないです。

生成したコードを Git にコミットする

私たちのプロジェクトでは、先ほど例に挙げた SVGR の他に、 GraphQL Code Generator や typed-scss-modules などのコード生成のためのライブラリを利用しています。
これらはすべて、 Git にコミットして GitHub 上で確認できるようにしています。

やったこと

もちろん .gitignore に登録しなければよいだけですが、あわせて次のような対応をしています。

  • ESLint や Prettier の検査対象から外す
  • 生成されたコードが最新バージョンか CI で確認する
  • PR File Changes の中で生成されたファイルの差分をデフォルトで非表示にする

ESLint や Prettier の検査対象から外す

ESLint や Prettier の検査対象にしてしまうと、生成したのちに手動または自動でフォーマットしたり、意図しないエラーを防ぐために ESLint のルールを追加したりする必要あります。
生成したコードは一切触らないで済むように、 ESLint の ignorePatterns[2] に設定したり Prettier の ".prettierignore"[3] を作成して、検査対象から外しました。

生成されたコードが最新バージョンか CI で確認する

生成されたコードの更新を忘れてしまうと、最悪リリースまで気づけずに大きな問題を引き起こしてしまう可能性があります。

差分の検知は git diff の機能で検知できます[4]
次のようなワークフローを用意して、コードに差分に気付けるような仕組みを作りました(pnpm generate は、すべてのコード生成プロセスを実行するコマンドです)。

.github/workflows/lint.yml
name: lint
on:
  push:
    branches:
      - main
  pull_request:
permissions:
  contents: read
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          cache: 'pnpm'
          node-version-file: '.node-version'
      - run: pnpm install
      - name: Verify the generated codes by `generate` is up-to-date
        run: |
          pnpm generate
          git diff --exit-code

PR File Changes の中で生成されたファイルの差分をデフォルトで非表示にする

".gitattributes" というファイルを指定することで、ファイルの差分を非表示にできます[5]。以下の例は、 GraphQL Code Generator で生成されるファイルを非表示に設定したものです。

.gitattributes
src/gql/** linguist-generated

生成されたファイルは基本的には他のファイルの変更を反映しているだけです。元のデータの数行の変更が生成されたファイルの何十行もの変更として反映されることもあります。
File Changes の差分が大きくなるとレビュアーとレビューイのどちらにとっても心理的ハードルが高くなるので、必須の対応だと感じています。

✅ よかった点

生成されたコードが GitHub 上で見える / コメントができる

元データは意図した通りの実装になっているが生成したコードが意図してない結果になる可能性があります。とくに、ライブラリを導入した最初期だと config の設定が十分に練られていない可能性があるので、生成したコードを注意深く確認したくなります。
Git で管理していると GitHub の PR に差分が表示されるので、確認したりコメントをつけたりできます。

また、生成されたコードがどのように変化しているか Git で辿ることができるようになるので、万が一バグが生じたときにも追いやすくなります。

基本的には生成されたコードは元データを反映しているので、元データがレビューされていれば十分だと考えていますが、デフォルトで非表示にしているので気にならないです。

🤔 気になる点

マージコンフリクトが発生しやすい

(GraphQL Code Generator を client preset で利用する場合のように)プロジェクト全体に対して一つのファイルを生成するようなライブラリがあります。
このような場合、それぞれのメンバーが別のデータに変更を加えていてもコンフリクトが発生する可能性があります。
こればかりはどうしようもないので、なるべく PR の小さくするなどの運用の工夫を取り入れたり、メンバー間で状況を共有し合うなどの工夫ができると解消できると考えています[6]

ライブラリのアップデートで CI がコケる

明示的な生成のデメリットとして挙げたものと似ていますが、ライブラリのアップデートで CI がコケるケースがあります。具体的な例でいうと、ここ数週間の間に何度か、 Renovate のライブラリの更新によって SVGR が生成した React コンポーネントの数値に微妙な変更が加わり、それが原因で CI がコケてしまいました。その度にコードを再生成します。
対応がやや面倒くさいと感じるものの、その都度気づいて確認できるので、意図しない変更がリリースされるよりもマシかなと感じています。

まとめ

自動生成ライブラリを利用するときにコードを明示的に生成して、さらに Git で管理する運用についてまとめました。いくつか気になる点をあげましたが、セットアップと保守の簡単さという点でメリットの方が十分に大きいと感じています。

脚注
  1. パスやファイル名はカスタマイズできます ↩︎

  2. https://eslint.org/docs/latest/use/configure/ignore ↩︎

  3. https://prettier.io/docs/en/ignore.html ↩︎

  4. https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---exit-code ↩︎

  5. 参考: https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github ↩︎

  6. むしろ、マージコンフリクトが頻繁に発生する場合は PR が大きすぎるなどの運用上の問題を抱えている可能性があるので、コンフリクトのおかげで気付けたとポジティブに考えることもできますね ↩︎

Discussion