タスクランナーを自作してみよう

`npm run scripts`のお話です。

ここ数年、「タスクランナーに疲弊する」的なお話を聞くようになってきました。

- [Why I Left Gulp and Grunt for npm Scripts](https://medium.freecodecamp.com/why-i-left-gulp-and-grunt-for-npm-scripts-3d6853dd22b8#.8il4n2yck)
- [Grunt/Gulpで憔悴したおっさんの話](https://t32k.me/mol/log/npm-run-script)
- [gulp なしの Web フロントエンド開発](http://akabeko.me/blog/2015/08/dev-web-front-end-without-gulp/)

とはいえすでに喧々諤々とした議論が交わされていますし、ググればもっと色々な記事に出会えるお話です。
そこで今回は、実業務から離れた上でのnpm-run-scriptの有用性についてお話できればと思っています。

## CLIやAPIは良くできている
普段タスクランナーを使用していると、プラグインのドキュメントで完結してしまいがちです。
しかしそのプラグインを介して使用するライブラリに目を向けてみると、当然ライブラリ自体にCLIやAPIがあります。そしてそのCLIやAPIがなかなかどうして良くできていたりするのです。

## npm-run-scriptでお勉強
`プラグインのコンフィグ===ライブラリのコンフィグ`は保証されていません（当たり前ですが）。
個人で管理しているリポジトリで脱タスクランナーをしてみるとそれを実感できます。CLIやAPIがどうなっているか、ライブラリ自体がどうなっているかのお勉強を進めるとプラグインの理解も進みますし、それがお仕事でのつまづきを減らすことに繋がったりもします。

## npm-run-scriptの限界と可能性
たとい個人のリポジトリといえど、CIや開発などいろいろなパターンに対応したnpm-run-scriptを考えていくと、どうしても自前で用意したいフラグが増えていきます。
そうなると、まず始めにCLIに限界を感じ始め、APIの利用が多くなります。
このあたりまでくると、以下のようなrun-scriptがpackage.jsonに書かれているはずです。

```
node tools/build
```

一種の自作タスクランナーですね。上記の通りライブラリ自体のCLIやAPIを利用しNode.jsで組み立てると、ライブラリのお勉強は言わずもがなNode.jsのお勉強もできてしまいます。
私もよくこんな感じでbuild-scriptを作っていたのですが、ここ数ヶ月Reactをお仕事で使っていて、Reactのスターターキットを漁っていたところ、なかなかに良いbuild-scriptを使っているキットに出会いました。

- [react-starter-kit](https://github.com/kriasoft/react-starter-kit)

かなり有名なリポジトリです。これのタスクランナーが非常によくできて、可能性を感じたので紹介したいと思います。

## react-starter-kitの紹介

まず構成をシンプルにあらわすと以下のようになります。

```
tools/
  build.js
  ...
  run.js
  ...
```

`run.js`がタスクランナーです。
`babel-node tools/run build`というような感じで使用します。
まず、この使用方法がよいなと思いました。
`run.js`はコード量が30行にも満たない非常にシンプルなもので、タイムスタンプを押してモジュールを実行し、エラーをハンドリングする、ということしか行っていません。ただし、引数で受け取るスクリプトは`export default...`している必要があり、これがpromise（もしくはasync）である必要があります。
ちなみに`babel-node`なのはES modulesとasync/awaitのためだと思われます。async/awaitはNode.js\^7.0.0でExperimentではありますが使用することができるので、ただのNode.jsへの置き換えも比較的簡単かもしれません。

```js
// run.js（簡略化してあります）
function run(task, options) {
  const start = new Date();
  console.log(start);
  return task(options).then(resolution => {
    const end = new Date();
    const time = end.getTime() - start.getTime();
    console.log(time);
    return resolution;
  });
}

if (require.main === module && process.argv.length > 2) {
  const module = require(`./${process.argv[2]}.js`).default;
  run(module).catch(err => { console.error(err.stack); process.exit(1); });
}

export default run;
```

続いて、`build.js`を紐解いてみましょう。`build.js`は以下のようになっています。

```js
// build.js（簡略化してあります）
import run from './run';
import clean from './clean';
import copy from './copy';
import bundle from './bundle';

async function build() {
  await run(clean);
  await run(copy);
  await run(bundle);
}

export default build;
```

async/awaitのおかげでだいぶリーダブルになっていますね。
**`run.js`を起点とし、全てのスクリプトを非同期で実行するというとてもシンプルなタスクランナーであることがわかります。**

`run.js`が循環に陥っていないのは、

```js
if (require.main === module && process.argv.length > 2) {
  const module = require(`./${process.argv[2]}.js`).default;
  run(module).catch(err => { console.error(err.stack); process.exit(1); });
}
```

この部分がモジュールで読み込まれたのかNode.jsから実行されたのかの判断を行っているためです。

##まとめ
業務において脱gulp化するかなどは、正直そのお仕事がどのようなものかによって変わってくると思っています。
ただ、個人でライブラリを作ったり、勉強のために何かしている方は、一度タスクランナーを自作してみては如何でしょうか。
上記のように良いツールを見つけたらコードリーディングを兼ねて導入してみると、色々と分かることもあると思います。

~~個人で作っているものにまでgulpを使って結局疲弊するなんて、元の木阿弥ですしね。~~