[WIP] npm-scripts を使い倒そう!

※ 書きかけです

## パッケージ依存管理ツール npm

[npm][npm] は、今や Web 開発には欠かすことのできないパッケージ依存管理ツールです。
[Browserify][browserify], [Babel][babel], [Gulp][gulp], [LESS][less] のようなツール、[Koa][koa], [Express][express] のようなサーバー側のライブラリ、 [React][react], [AngularJS][angular] のようなクライアント側のライブラリまで揃っています。
サーバー側はもちろん、クライアント側の依存管理も Browserify を併用することでスマートに実現できます。

そんな npm ですが、実はタスク ランナーの機能も持ち合わせています。
Grunt や Gulp の影に隠れて目立たない存在でしたが、[Substack の記事](http://substack.net/task_automation_with_npm_run)からこっち、[注目を集め始めて](http://www.google.co.jp/trends/explore#q=npm%20run)います (たぶん)。


## タスク ランナー npm

タスク ランナーといえば、Web 界隈では Grunt や Gulp が有名です。

これらは環境構築・ビルド・デプロイ等、目的に応じた定形処理(タスク)を登録しておき、コマンドひとつで実行できるようにしておくツールです。
それに加えて、ツールたちはタスクを定義しやすくする様々な工夫を競っています。

さて、タスク ランナーとしての npm はとてもシンプルです。
`package.json` (パッケージの情報や依存関係が書かれるファイル) の `scripts` フィールドに、タスク名とコマンドのペアを記述します。

```json:package.json
{
    "scripts": {
        "hello": "echo \"Hello World!\""
    }
}
```

定義したタスクは`npm run タスク名`で実行できます。

```:shell
$ npm run hello

> echo "Hello World!"
Hello World!
```

コマンドの内容は単なる Shell です。
Linux では `/bin/sh`, Windows では `cmd.exe` で実行されます。
npm ではたくさんの小さなコマンドライン プログラムが公開されているので、それらを活用して書いていきます。
パッケージ管理ツールとしての側面を活かしたシンプルな作り、というわけですね。


## 具体例

こんなにシンプルで、果たして使えるのか?

もちろん、プロダクトの方針にも依ると思いますが、私が試してみた感じでは、じゅうぶんに使えると思えました。
いくつか例を見て行きましょう。

※ 以下で紹介するスクリプト群は、基本的に Windows で動作確認しています。
　 単発テストに関しては Travis CI 上の Linux でも動作確認しています。

### ツールやサーバー側ライブラリの開発

```json:package.json
{
  "scripts": {
    "clean": "rimraf lib",
    "lint": "eslint src",

    "build": "npm-run-all clean lint build:babel",
    "build:babel": "babel src --out-dir lib",

    "test": "npm-run-all build test:mocha",
    "test:mocha": "mocha test/*.js --compilers js:espower-babel/guess --colors",

    "testing": "npm-run-all clean --parallel testing:*",
    "testing:build": "npm run build:babel -- --watch --source-maps-inline",
    "testing:mocha": "npm run test:mocha -- --watch --growl",
  }
}
```

```:shell
$ npm i --save-dev babel eslint espower-babel mocha npm-run-all power-assert rimraf
```

このスクリプト構成例は、`clean`, `lint`, `build`, `test`, `testing` タスクを定義します。

- **clean** -- ビルド結果のディレクトリを削除します。
- **lint** -- [ESLint][eslint] を用いて、ソースコードを静的検証します。
- **build** -- [Babel][babel] を用いて、ES2015 で書かれたソースコードを ES5 にトランスパイルします (その直前に `clean`, `lint` を実行します)。
- **test** -- [Mocha][mocha] + [PowerAssert][power-assert] を用いた自動テストを実行します (その直前に `build` を実行します)。
- **testing** -- `test` をウォッチ モードで実行します。コードを編集して保存すると即座にビルドされ、テストが再実行され、結果がデスクトップ通知で報告されます。
  - Windows では [Growl for Windows][growl-for-windows] を予めインストールしておく必要があります。
  - インストールしなくても、テスト自体は動作します。

もちろん、依存パッケージを `--save-dev` で保存しておくことで、他の環境では `npm i` コマンド一発ですべてダウンロードされて使えるようになります。

### クライアント側ライブラリの開発

```json:package.json
{
  "scripts": {
    "clean": "rimraf lib dist",
    "lint": "eslint src",

    "build": "npm-run-all clean lint build:*",
    "build:lib": "babel src --out-dir lib --source-maps-inline",
    "build:dist": "mkdirp dist && browserify lib/index.js --debug -t babelify --standalone __LIBRARY_NAME__ > dist/__LIBRARY_NAME__.js",
    "build:dist-min": "mkdirp dist && browserify lib/index.js -t babelify --standalone __LIBRARY_NAME__ | uglifyjs - --compress --mangle > dist/__LIBRARY_NAME__.min.js",

    "test": "npm-run-all clean lint build:lib test:karma",
    "test:karma": "karma start karma.conf.js --single-run",

    "testing": "npm-run-all clean --parallel testing:*",
    "testing:build": "babel src --out-dir lib --watch",
    "testing:karma": "karma start karma.conf.js --auto-watch --reporters growl,progress"
  }
}
```
```js:karma.conf.js
module.exports = function(config) {
  config.set({
    basePath: "",
    frameworks: ["browserify", "mocha"],
    files: [
      "node_modules/babel/node_modules/babel-core/browser-polyfill.js",
      "test/*.js"
    ],
    browsers: ["Chrome", "Firefox", "IE"],

    preprocessors: {
      "test/*.js": ["browserify"]
    },
    browserify: {
      debug: true,
      extensions: [".js"],
      transform: [
        "babelify",
        "espowerify"
      ]
    }
  });
};
```
```:shell
$ npm i --save-dev babel babelify browserify eslint espowerify karma karma-browserify karma-chrome-launcher karma-firefox-launcher karma-ie-launcher karma-mocha mkdirp mocha npm-run-all power-assert rimraf uglify-js
```

> ※ `__LIBRARY_NAME__` には、実際の名前を入れてください。

一気に長くなってしまいました。
だいたい [Browserify][browserify] と [Karma][karma] のせい。

このスクリプト構成例は、`clean`, `lint`, `build`, `test`, `testing` タスクを定義します (前項と同じです)。

- **clean** -- ビルド結果のディレクトリを削除します。
- **lint** -- [ESLint][eslint] を用いて、ソースコードを静的検証します。
- **build** -- この工程は少し複雑です。  
  - ふたつのディレクトリ `lib`, `dist` を生成します。
  - `lib` には、[Babel][babel] を利用して、ライブラリ利用者が [Browserify][browserify] で依存管理をする場合のための成果物を生成します。
  - `dist` には、[Browserify][browserify], [Babel][babel], [UglifyJS][uglify-js] を利用して、ブラウザから直接読み込んだり、[RequireJS][requirejs] で依存管理をする場合のための成果物を生成します。
- **test** -- [Karma][karma] + [Mocha][mocha] + [PowerAssert][power-assert] を用いた自動テストを実行します (その直前に `build:lib` を実行します)。
- **testing** -- `test` をウォッチ モードで実行します。コードを編集して保存すると即座にビルドされ、テストが再実行され、結果がデスクトップ通知で報告されます。

**TODO**

- [Travis CI](https://travis-ci.org) でテストできない
- [testling](https://ci.testling.com) 試したい
- 他のランナー ([Testem][testem]とか[Protractor][protractor]とか) も試したい
- Isomophicのテスト (ブラウザと Node で同時実行するランナー無いか?)

### Webアプリケーション開発

**TODO**

検証中です。
上記2つの内容と大きくは変わらず、[cpx][cpx] を利用した単純コピーと [node-dev][node-dev] あたりを利用したサーバー実行が入ると予測。
あと API テストの方針とか...

## まとめ

**TODO**

- ツールはまず CLI を持っている事が多いので、他のタスク ランナーと比較して、専用ラッパーへの依存を減らせる場合がある。
- `npm` で単一責務の小さなツールを探して or 作って公開して、パズルのように組み合わせて使うのは楽しい。一部だけ交換するのが楽なので、進化ペースを上昇させられる。
- Shell は stdin/out による Streams ベースのプラットフォームで、[Gulp][gulp] に似ている。

## [おまけ] この記事に登場したパッケージ紹介

### npm 一般

- **[npm-run-all][npm-run-all]** -- 拙作。複数の npm-scripts を順番 or 並列に実行する CLI.

### ファイル操作系

- **[mkdirp][mkdirp]** -- Substack (Browserifyのオーナー) 作。クロスプラットフォームの `mkdir -p`. ディレクトリが既存の場合は何もしない。
- **[rimraf][rimraf]** -- Isaacs (npmのオーナー) 作。クロスプラットフォームの `rm -rf`.
- **[cpx][cpx]** -- 拙作。変更監視できる `cp` コマンド。Glob記法で指定したファイルの変更監視をして、変更時に指定ディレクトリへコピーする (ディレクトリ ツリー構造を保ったまま)。Browserify 互換の変換モジュールを噛ませられる。
- **[catw][catw]** -- Substack 作。変更監視できる `cat` コマンド。Browserify 互換の変換モジュールを噛ませられる。
- **[watch][watch]** -- Isaacs 作。指定ディレクトリでファイル更新があった際に任意の Shell コマンドを実行させられる。

### JavaScript 変換系

- **[babel][babel]** -- 超人 sebmck 作。EcmaScript の標準委員会で検討されている新仕様を ES5 で動くコードに変換してくれるツール。
- **[Browserify][browserify]** -- Substack 作。Node.js の `require` の Lookup の仕様をフロントエンドに持ち込めるツール。
- **[UglifyJS][uglify-js]** -- 有名なミニファイ ツール。未使用コードの削除も行ってくれる (重要)。

### テスト系

- **[ESLint][eslint]** -- 静的検証ツール。ESTree 仕様に則った抽象構文木を対象とした、さまざまな検証ルールをプラグインできる。組み込みのルールもきめ細やかな設定が可能。ES2015 や JSX にも対応している。
- **[Karma][karma]** -- テストランナー。複数のブラウザを立ち上げてその中でテストを実行し、結果をコンソールに出力してくれる。
- **[Mocha][mocha]** -- 人気のテストハーネス。
- **[PowerAssert][power-assert]** -- twada 作。アサーション ライブラリ。事前の変換によって、アサート式が失敗した時に詳細な情報を出力してくれる。便利。
- **[espower-babel][espower-babel]** -- azu 作。[Mocha][mocha] プラグイン。テストコードに対して [Babel][babel] + [PowerAssert][power-assert] の変換をしてくれる。
- **[espowerify][espowerify]** -- twada 作。Browserify の変換モジュール。[PowerAssert][power-assert] の変換をしてくれる。


  [angular]: https://www.npmjs.com/package/angular
  [babel]: https://www.npmjs.com/package/babel
  [browserify]: https://www.npmjs.com/package/browserify
  [catw]: https://www.npmjs.com/package/catw
  [cpx]: https://www.npmjs.com/package/cpx
  [eslint]: https://www.npmjs.com/package/eslint
  [espower-babel]: https://www.npmjs.com/package/espower-babel
  [espowerify]: https://www.npmjs.com/package/espowerify
  [express]: https://www.npmjs.com/package/express
  [growl-for-windows]: http://www.growlforwindows.com/gfw/
  [gulp]: https://www.npmjs.com/package/gulp
  [karma]: https://www.npmjs.com/package/karma
  [koa]: https://www.npmjs.com/package/koa
  [less]: https://www.npmjs.com/package/less
  [mkdirp]: https://www.npmjs.com/package/mkdirp
  [mocha]: https://www.npmjs.com/package/mocha
  [npm]: https://www.npmjs.com
  [npm-run-all]: https://www.npmjs.com/package/npm-run-all
  [power-assert]: https://www.npmjs.com/package/power-assert
  [protractor]: https://www.npmjs.com/package/protractor
  [react]: https://www.npmjs.com/package/react
  [requirejs]: http://requirejs.org
  [rimraf]: https://www.npmjs.com/package/rimraf
  [testem]: https://www.npmjs.com/package/testem
  [uglify-js]: https://www.npmjs.com/package/uglify-js
  [watch]: https://www.npmjs.com/package/watch