このポストは、Why I Left Gulp and Grunt for npm Scriptsを筆者の許諾を得て意訳したものです。間違いがありましたら、ご指摘いただけると幸いです。
(以下、訳)
私はGulpとGruntが不要な抽象化レイヤーだと気づきました。npmのscriptsはとても強力で、そっちの方が便利だったりします。
例を挙げましょう
私はかつてはGulpが大好きでした。しかし結局のところ、100行ものgulpfileと大量のgulpプラグインを扱うハメになりました。Gulp上でWebpackやBrowsersync、Mochaなどを統合するのは本当にたいへんでした。なぜでしょうか?それは、プラグインによってはドキュメントが不十分だったり、APIの一部しか公開されていなかったためです。
これらを解決しようと思えばできました。しかしなんと それらのツールを直接使用すると不具合が起こらなかったのです。
私は最近、多くのオープンソースのプロジェクトがnpm scriptsを使っていることに気づきました。本当にgulpが必要なんでしょうか?そう立ち返ったとき、もはや不要なのだと私は気付きました。
それからというもの、新しくプロジェクトを始めるときにはnpm scriptsを使うと決めたのです。Reactアプリケーション開発のための環境とビルドプロセスを、npm scriptsだけを使って構築しました。気になる方はこちらをご覧ください。
驚くべきことに、いまではGulpよりもnpm scriptsで作業するほうが快適になってしまいました。
GulpとGruntの何が問題なのか
長らく考えた結果、gulpやgruntのようなタスクランナーには主に3つの問題があると気づきました。
- プラグイン作者への依存
- デバッグでのストレス
- バラバラのドキュメント
順番に見ていきましょう。
1.プラグイン作者への依存
あなたが新しい技術や、無名の技術を使っているとしましょう。おそらくそのためのプラグインはまだ存在しないかもしれません。あるいは、プラグインが存在したとしても、それは時代遅れである可能性があります。
例えばBabel6が最近リリースされましたが、APIが劇的に変更されてしまったため、たくさんのgulpプラグインが互換性を失ってしまいました。Gulpを使おうとしたら、プラグインがアップデートされていないためにハマってしまってしまったのです。
GulpやGruntを使うにあたっては、プラグインの更新を待つか、あなた自身がプラグインを修復しなくてはなりません。これは、あなたを失速させてしまいます。一方で、 npm scriptsを使った場合は、余計な抽象化レイヤーを噛ませることなく、ツールを直に使うことができます。 つまり、新しいバージョンが出るやいなや、そのツールをただちに使うことができるのです。
プラグインの豊富さという観点では、npmに優るものはありません。
( gulpには2100,gruntには5400, npmには227,000ものパッケージが存在します。さらにnpmには毎日400ものパッケージが誕生しています。)
npm scriptsを使えば、わざわざgulpやgruntのプラグインを探す必要はなくなるのです。
もちろん、GruntやGulpのプラグインがダメになったら直接npm scriptsを使えば良い話です。ただし、それはもはや、そのタスクをgulpやgruntに任せることはできなくなったということを意味します。
2. デバッグのストレス
gulpやgruntでパッケージの統合がうまくいかないとき、原因調査は大変です。gulpやgruntという余計な抽象化レイヤーを1枚増やして作業をしているので、その分バグが潜みやすくなります。
- そもそものツールが壊れているのか?
- grunt/gulpのプラグイン自体が壊れているのか?
- 設定ファイルに問題があるのか?
- 互換性のないバージョンを使っているのか?
npm scriptsを使えば、上記のうち2つ目の可能性が排除されます。かつ、3つ目の可能性もそこまで高くないとわかりました。というのも、ターミナルで直接実行して確かめてみたので。そして4つ目については、タスクランナーの代わりにnpmを直接使用してパッケージの数を減らすことで対応できました。
3.バラバラなドキュメント
gulpやgruntのプラグインをツール本体と比較した時、残念ながらそのドキュメントは乏しいものとなっています。例えば、gulp-eslintを使っているとします。その場合、gulp-eslintのドキュメントと、eslintのサイトを行ったり来たりして時間を奪われることになります。それに、プラグインとツールとでは文脈が異なります。
gulpやgruntを使うには、ツールそのものを理解するだけでは不十分です。プラグインによる抽象化への理解も求められます。
それがgulpやgruntの要件です。
多くのビルドツールは、素晴らしいドキュメントが記されたCLIを提供します。eslintのCLIのドキュメントを見てみましょう。非常にわかりやすくデバッグもしやすいです。(それはgulpやgruntのようなレイヤーが欠落している為です)
さて、デメリットを挙げたことですし、なぜ私達はgulpやgruntのようなタスクランナーが必要だと考えてしまうのか、について検証していきましょう。
なぜ私たちはnpmを蔑ろにしたのか
以下の4つの誤解がgulpやgruntを優位にしてしまいました。
- npm scriptsは難しいコマンドスキルを要する
- npm scriptsはそれほどパワフルではない
- gulpのストリームは、高速なビルドには不可欠である
- npm scriptsはクロスプラットフォームではない
これらの誤解について、順番に検証していきましょう。
誤解1. npm scriptsは難しいコマンドスキルを要する
npm scriptsの真価を堪能するのに、難しいOSコマンドの知識は不要です。もちろんgrep, sed, awk, pipeは一生かけて学ぶ価値がありますが、だからと言って、UnixやWindowsの魔術師になれと言っているのではありません。その代わり、よーくドキュメント化されたたくさんのnpm scriptsを利用することで、業務を完遂することができます。
例えば、Unixにおいて、ディレクトリを強制的に削除するrm -rf
というコマンドを知らないとしても、rimrafがまったく同じ挙動をしてくれます。(しかもクロスプラットフォームで動作します。)多くのnpm パッケージは、コマンドの知識が少なくとも大丈夫です。ただ、必要なパッケージを検索して、ドキュメントを読み、使って試せばいいのです。私はかつて、gulpのプラグインを検索していました。けれど今では、npmのパッケージを検索しています。ここに素晴らしい資料があります。
誤解2. npm scriptsはそんなにパワフルではない
npm scriptsはそれら自体が驚くべきほどパワフルです。以下に示すのは、事前・事後フックの例です。
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"prebuild": "echo I run before the build script",
"build": "cross-env NODE_ENV=production webpack",
"postbuild": "echo I run after the build script"
}
}
上記のスクリプトにおいて、prebuildはbuildという名前をもつビルドスクリプトの前に走ります。(ただし、他にpreがついたスクリプトがある場合は例外です。)そしてpostbuildは、buildの後に走ります。なので、もしprebuild→build→postbuildというscriptsを定義したならば、npm run build
とコマンドするだけで、それらが順番通りに実行されます。
次に、以下の例を見てください。
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"clean": "rimraf ./dist && mkdir dist",
"prebuild": "npm run clean",
"build": "cross-env NODE_ENV=production webpack"
}
}
上記の例では、prebuildがcleanを呼び出します。これによってscriptsを、より小さなワンライナーへと分解することができます。しっかりと責務分割を行い、適切な名前をつけることが可能となるのです。
また、&&
を使うことで、複数のscrptsを1行で呼び出すこともできます。上記の例では、crean内に2つのscriptがあり、順に実行されます。もし、gulpでのタスク順に悩まされているのなら、ここで紹介した簡潔さが救いとなるはずです。
そしてもし、コマンドが複雑になりすぎた場合でも、別ファイルとして呼び出すことができます。
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"build": "node build.js"
}
}
上記のbuildタスクでは、別のscriptsを呼んでいます。そして呼ばれたscriptsはNodeによって実行されるので、どんなnpmパッケージでも利用することができます。つまり内部的にはJavaScriptの力をも利用しているのです。
他の実例については以下を参照ください。
npm the core features are documented.
Pluralsight course on using npm as a build tool.
誤解3. gulpのストリームは、高速なビルドには不可欠である
gulpは直ぐにgruntへの注目を奪っていまいました。というのも、gulpのメモリ内ストリームは、gruntのファイルベースのそれよりも速かったためです。しかし、ストリームを利用するのにgulpは不可欠ではありません。事実、ストリームはUnixとWindowsのコマンドラインにずっと組み込まれてきました。パイプ(|)はあるコマンドの出力を、別のコマンドの入力として宛てがいますし、リダイレクト(>)は出力をファイルに渡します。
なので、例えばgrepを使ってファイル内を検索して新しいファイルに出力する、なんてこともできるのです。
grep ‘Cory House’ bigFile.txt > linesThatHaveMyName.txt
上記のストリームが実行されても、中間ファイルは記述されません。
(どうやってそんなことが?とお思いですか?では、読み進めてください…)
また、&
を使って、2つのコマンドを同時に叩くこともできます。
npm run script1.js & npm run script2.js
上記の2つのコマンドは同時に走ります。連続してスクリプトを走らせるには、npm-run-allをお使いください。ただし、これは次の誤解を招きます。
誤解4. npm scriptsはクロスプラットフォームではない
多くのプロジェクトは、特定のOSに縛られています。なので、クロスプラットフォームについての心配は無用です。もしクロスプラットフォームが必要になったとしてもnpm scriptsは立派に動作します。多くのオープンソースプロジェクトがそれを証明しています。
LinuxとOSXならばnpm scriptsはUnixのコマンドラインで動作しますし、WindowsならばWindowsのコマンドラインで動作します。要するに、すべてのプラットフォームでビルドスクリプトを走らせたいならば、UnixとWindowsでの実行条件を満たす必要があります。以下、3つの手法を紹介します。
手法1. クロスプラットフォームで動作するコマンドを使う。
クロスプラットフォームで動作するコマンドはたくさんあります。以下に例を示します。
&& chain tasks (Run one task after another)
< input file contents to a command
> redirect command output to a file
| redirect command output to another command
手法2. nodeのパッケージを利用する。
シェルコマンドの代わりにnodeのパッケージを使うこともできます。例えば、rm -rf
の代わりにrimrafを使うことができます。あるいは、cross-envを使うことで、クロスプラットフォームで環境変数を設定できます。検索すれば必要なパッケージはほぼ確実に見つかるでしょう。そして、もしコマンドが長すぎる場合には、以下のようにNodeパッケージからscriptを呼び出すこともできます。
node scriptName.js
上記のコードはNodeで実行される、平易でありきたりなJavaScriptです。そして、コマンドラインでscriptを呼んでいる以上、jsファイル以外も実行可能です。Bash, Python, Ruby, Powershellなど、OSが実行できるscriptならば実行することができます。
手法3. ShellJSを使う。
ShellJSはNode経由でUnixコマンドを実行する、npmパッケージです。そのため、Unixコマンドをどんなプラットフォームでも実行することができます。もちろん、Windows上でもです。
私が作成したReact Slingshot.では、1と2の手法を組み合わせています。
デメリット
npm scriptsの利用には、デメリットがあることも認めなくてはなりません。例えばJSONファイルにコメントを書くことはできません。なので、package.jsonにコメントを加えることは不可能です。この回避策としては、以下のものが考えられます。
- 簡潔で、適切な名前を持った、単一責務のscriptを心がける。
- 別途、ドキュメント化する(例.READMEに追記する)
- 切り分けたjsファイルを呼び出す。
私は、1の回避策を好みます。scriptを単一責務レベルまでしっかりと分割すれば、コメントがほぼ不要になるからです。 小さく、適切な名前付けをされた関数のように、scriptもまた適切に自己を表す名前を持つべきです。 私が“Clean Code: Writing Code for Humans”で論じたように、小さく責務分割された関数はコメントを必要としない場合がほとんどです。コメントが必要だと感じるのは、3の回避策を取り、scriptsを別ファイルに書き移す時です。別ファイル化によって、いつでもJavaScriptの持つ部品化の力を享受することができます。
また、Package.jsonは変数をサポートしていません。これは大きな問題のよう思えますが、実はそうでもないのです。それには2つの理由があります。まず第一に、変数に対するだいたいのニーズは、コマンドライン上の環境設定で吸収できます。そして第二に、もし他の理由で変数が必要だとしたら、別のjsファイルを呼び出せば良いのです。このパターンにおけるすばらしい実例については、React-starter-kitをご覧ください。
最後に、難解で冗長なコマンドライン引数にはリスクがあります。コードレビューや丹念なリファクタリングは、わかりやすいnpm scriptsを保証する素晴らしい手段です。もしコメントが必要なほど複雑になったしまったとしたら、リファクタリングしてscriptを分割する必要があります。
抽象化は適正化されるべき
gulpやgruntは私の使うツールを抽象化するものです。抽象化は役に立ちますが、コストがかかるものでもあります。gulpやgruntを使うことで、プラグインの制作者やドキュメントへの依存性が増します。そして、依存性の増大は、複雑性を呼びます。私は、gulpやgruntのようなタスクランナーはもはや不要な抽象化であると判断しました。