最近のWebフロントエンド開発は、メタ言語やトランスコンパイル前提のコードをビルドして、ブラウザで動かすためのファイルを自動生成するスタイルが当たり前になっています。

Gitでそのプロジェクトを管理する場合、ビルドによって生成されたファイルの管理は厄介なので除外するのが普通ですが、事情によってはそれもリポジトリに含めて管理しなければならないこともあります。

受託開発では、最初の納品以降は差分があったファイルのみを納品するというパターンがあります(つらい)。ビルドされたファイルをGitで管理していると、Gitのコマンドで差分を取って変更があったファイルのみを納品用ZIPとして生成ということができますが、Gitに含めていないと差分を取るのが手間になります。

そういった理由で、ビルドの生成物をGitリポジトリに止む無くコミットしているわけですが、実際に起こりうる問題としては以下のようなものがあります。

問題点

git logに無駄なファイルが入る

開発上で変更のログを残したいのは元ソースの変更だけのはずです。例えばSassのある変数の値を変更したとき、変数の値を変更したということだけが記録されていればよく、生成されたCSSのどの箇所が影響を受けたかという情報は邪魔なノイズにしかなりません。

これのひどい例は、大量のファイルから参照されているメタファイルの一部が書き変わったことで、ビルド後に大量のファイルが変更され、そのコミットでそもそも何を変更したのかがわかりにくくなることです。

元ファイルの変更が簡潔に記録されていれば、生成されるどのファイルが影響を受けるかは把握できる(あるいは関心を持つ必要がない)はずです。

生成後のファイルを直接書き換えてしまう

どれが元ファイルかわからず、ビルドによってのみ生成されるべきファイルを直接書き換えた結果、さまざまなトラブルが起きて無駄な手間が生まれることがあります。

そのやり方に慣れれば問題にはならないかもしれませんが、そもそも間違いを誘発するような構造は排除するべきです。

コミットごとに元ファイルとビルド後のファイルの変更が対応しない

元ファイルを変更するたびにビルドが実行されずに、コミットごとに元ファイルとビルド後のファイルが対応していない状態になりがちです。毎回コミット前にビルドコマンドを叩くという風にするのは手間で、pre-commitのフックを利用するにしてもその待ち時間がストレスです。

仮にそのルールを徹底できても、次のような問題もあります。

依存モジュールのバージョン違い

そのとき利用しているNode.jsのバージョン、また、npm installを実行したときによって依存しているモジュールのバージョンが違っていて、元ファイルの内容は同じなのに、ビルドした人(環境)によって生成されるファイルが違う結果になることがあります。

改行コードの違い

.editorconfigなどを利用して元ファイルの改行コードを設定していても、ビルド結果に含まれる改行コードはそのシステムに依存していて、WindowsならCRLF、MacならLFとして無意味な変更をコミットしてしまいます。

環境によって微妙にビルド結果が違う

仮に依存モジュールのバージョンが全く一致していても、ビルド環境によって微妙に結果が違って、毎回ビルドするごとに無駄な変更をコミットしてしまうことがあります。

改善案

上記のような理由がビルドの生成物をリポジトリに含めることの問題ですが、いくつかの問題は次のような方法で改善することができます。

ビルドタスクの組み方を工夫する

コミット前にビルドコマンドを実行する手間を省くために、ビルドタスクの組み方を工夫することができます。

開発用のビルドと、本番用のビルドで生成されるファイルは異なっていることが多いです。例えば、開発時はSassをCSSにコンパイルするだけで、本番時はコンパイルした上で圧縮するという風にしたいことが多いです。

その場合、それぞれでファイルの生成先が同じになっていると、元ファイルを変更していなくてもdist/以下の内容が変わってしまいます。そのため、開発用に生成されたファイルは、Gitから除外された.tmp/などのディレクトリの中に生成し、ライブリロードなどの開発用サーバーからはそこのディレクトリを参照します。

そして、開発用コマンドの実行中、ファイルを変更すると、開発用と本番用の両方にビルドしたファイルを生成します。Gulpタスクとして書くと、以下のようなイメージです。

const gulp = require('gulp')
const sass = require('gulp-sass')
const csso = require('csso')

gulp.task('css', () =>
  gulp.src('src/css/main.scss')
    .pipe(sass())
    .pipe(gulp.dest('.tmp'))
    .pipe(csso())
    .pipe(gulp.dest('dist'))
)

これを例えば、

  • npm startを実行したときはファイルの変更を監視して開発用サーバーを立ち上げる
  • npm run buildを実行したときは単に一度ビルドを実行するだけ
  • 生成されるファイルは互いに同じ

という風に使います。

このやり方に従って、実際に僕が開発時に利用しているタスクはこのように書いています。

ただしこの場合、npm startを実行せずにファイルを変更した場合は、別途npm run buildを実行する必要があります。

.gitattributesを設定して改行コードを揃える

リポジトリのルート直下に.gitattributesを作成して、以下のように記述すれば、リポジトリ内のファイルの改行コードはLFに揃えられます。

* text=auto

携わる全ての開発者に適用されるので、git configで設定するよりも好ましいです。

依存モジュールのバージョンを固定する

npm shrinkwrapを実行すると、依存モジュールのバージョンがnpm-shrinkwrap.jsonに出力されます。出力されたnpm-shrinkwrap.jsonをリポジトリにコミットしておくと、別の開発者がnpm installした際に指定されたバージョンがインストールされ、バージョンが固定されます。

少し話が逸れますが、npmの代わりに利用できるパッケージマネージャーのyarnを利用すれば、npmよりも高速に依存モジュールのインストールができて便利です。その場合、yarn.lockというファイルが生成され、yarnを利用する場合は同じようにバージョンが固定できます。個人的には少し前からこっちに移行していますが、速くて便利です。

まとめ

差分納品はつらいですが、この仕組みは変えられなさそうです。この枠組みを受け入れて、その中でうまくできるように考えるのが前向きかなという感想です。