タスク・ランナーをnpm run-scriptでラップ

npm で依存もタスクも一元化するという記事を興味深く読んだ。僕もしばらく前、具体的にはnpm v2出た時からGruntをnpm run-scriptでラップして使っている。概ね良好に機能すると感じている。懸念であった引数で特定の処理を行わせたいようなケースもnpm v2で引数を解釈できるようになったので解決した。

npm run-script経由にすることによる大きなデメリットとしては、そんなに速くもないnpm経由で常にタスクを実行することによる速度の低下が挙げられる。

この速度の低下は、Gruntやgulpの主要な目的であるビルドにおいてはそれほど問題にならない。ビルドにかかる時間に比べると、相対的にその低下の割合は低いものだと考えられるからだ。しかしタスクはそういったものにとどまらず例えばHTML(やMarkdown)のプレビューであったり、Sassのオンデマンド・コンパイルといった小さなタスクもある。その場合は速度低下が占める割合は高くなってしまい、実行時のストレスに繋がる。

もちろんそういった小さなタスクを各開発者が自前で用意している環境に任せるという選択もあり、実際そうする方が良いことが多い。しかしその一方で小さなタスクまでも用意してやることによって、環境一式を開発者間で揃えられるというメリットも捨てがたい。その場合、小さなタスクのコストをできるだけ小さく抑えるための工夫が必要になる。

そうなるとタスク・ランナー経由で実行させるよりも、設定不要な小さなNode.jsスクリプトを作りそれを実行した方が効率的だ。プロジェクトにGruntとgulpを混ぜてnpm run-scriptでラップというのは馬鹿げているが、Gruntと小さなNode.jsスクリプトの組み合わせは悪くはない。そのNode.jsスクリプトが設定不要なこととpackage.jsonscriptで複雑なコマンドライン・オプションを指定したコマンドを定義しないこと辺りがキモになる。

{
  "private": true,
  "scripts": {
    "build": "grunt build",
    "deploy": "grunt deploy",
    "preview": ".bin/preview",
    "sassc": ".bin/sassc",
    "start": "grunt connect",
    "test": "grunt test"
  },
  devDependencies: {}
}

具体的にはこのような形でミックスしている。

$ npm test
$ npm run deploy
$ npm run preview -- src/weblog/drafts/example.markdown
$ npm run sassc -- src/css/style.scss

実行のインターフェイスは統一され、引数あるなしはあるものの内容には特に左右されない。例えばこの.bin/preview(20行くらい)では引数のファイルの拡張子でどうやってHTMLを作るかを分け、一時ディレクトリーにHTMLファイルを作成し、最後にそれをブラウザーで開くようになっている。また.bin/sassc(10行くらい)はSassのCLIプログラムをプロジェクトにあった形のオプションで実行するだけのラッパー・スクリプトだ。

両者ともに似たような、またはもっと高機能のGruntプラグインやgulpプラグインはいくつも存在する。それらは小回りを犠牲に汎用性を持たせているため、プロジェクト特化型の小さなNode.jsスクリプトと違い、やることに比して大仰過ぎる嫌いがある。まさに牛刀といったところだ。

無理にGruntやgulpに寄せるよりも、タスクの規模と利用ケースを考慮して書き分け、npm run-scriptで統一してやる方が使いやすいものになるはずだ。どうせ使う方は内容を読んだり修正したりはしないので、Node.jsで完結しているならばどんな風に書かれていても大した問題ではない。プロジェクトを構成するファイルが増えることにはなるが、.binといった見えにくいディレクトリーに放り込んでおけば邪魔扱いされることもないだろう。

僕はGruntやgulpといったタスク・ランナーをいずれ捨てる(新しいタスク・ランナーに変えるではなく、単に捨てる)ことも視野に入れて使っているので、特にこう感じるのかもしれない。


他にはnpm runは微妙に長いことと引数を付ける時に--を挟むのを忘れがちなことというような、使い方において慣れないと厳しいことくらいか。package.jsonの肥大化が~というような観点もあるが、多くても20行くらい(pre-post-を駆使した場合でも)なのでGruntfile.jsのあっという間に300行と比べると問題にならないだろう。

npm run-scriptでのラップが優れた解であることは疑いないが、その実際の構成にはもう少し色々考えるべきところがありそうだ。もっと様々なユースケースを知りたい。