npm-run-all と concurrently を試す

2015年8月10日 1 開発 , , ,

gulp なしの Web フロントエンド開発 のコメント欄にて npm-scripts を Windows 環境でも並列実行させる方法として npm-run-all を勧められた。また、他にも CLI 周りのツール一覧を教えていただき、その中にある concurrently も並列実行を実現するものなので、これらをまとめて試してみる。

  • 2015/9/1 追記
    npm-run-all v1.2.8 を試すにも書いたとおり、最新の npm-run-all であれば watchify の終了問題は発生しない。よって現在の package.json では npm-scripts の実行を npm-run-all に統一している。

元の npm-scripts

npm-run-all と concurrently による書き換え対象となる npm-scripts は以下。

{
  "scripts": {
    "server": "browser-sync start --server src",
    "watch:css": "watch \"npm run build:css\" ./src/stylus/",
    "watch:js": "watchify -v -t babelify ./src/js/App.js -o ./src/bundle.js -d",
    "watch": "npm run watch:css & npm run watch:js & npm run server",
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/bundle.css",
    "release:js": "browserify -t babelify ./src/js/App.js | uglifyjs > ./dist/bundle.js",
    "release:clean": "trash ./dist",
    "release:mkdir": "mkdirp ./dist && npm run release:clean && mkdirp ./dist",
    "release:copyfiles": "copyfiles -f ./src/*.html ./dist",
    "release:copydirs": "ncp ./src/fonts ./dist/fonts",
    "release:copy": "npm run release:copyfiles && npm run release:copydirs",
    "release": "npm run release:mkdir && npm run release:copy && npm run release:css && npm run release:js"
  }
}

watch:js は元記事でも触れたとおり watchify の -o オプションにおけるパイプ機能が Windows 非対応なので、Source Map ファイル出力を諦めて埋込み式にしている。Windows 環境への対応が不要なら、以下のようにするとファイル出力版になる。

{
  "scripts": {
    "watch:js": "watchify -v -t babelify ./src/js/App.js -o \"exorcist ./src/bundle.js.map > ./src/bundle.js\" -d"
  }
}

release については、単なる連結なので特筆すべきことはない。

npm-run-all

npm-run-all は npm-scripts の連結実行を管理するためのパッケージである。

README を読むと直列・並列実行をサポートし、コマンドも呼び差しも scripts に定義した名前で指定できる。Node モジュールからコマンドを処理することでシェル依存を回避し、結果として Windows 環境でも動作する。

また、直列・並列実行は混在させられる。まるで run-sequence みたいだ。gulp でも run-sequence は重宝しているので、これに相当する機能を CLI から利用できるのはありがたい。

元記事では並列実行だけを問題としていたが、直列であっても連結がシェル依存だと別の問題に遭遇するかもしれない。よって npm-scripts の連結全般を npm-run-all に担当させてみる。

watch

watch の並列実行を npm-run-all に置き換えた。scripts を並列実行させる場合は、対象を独立した scripts として定義しておき、それらを -p オプションの後に並べる。npm run を省略できるため、見た目もスッキリしている。

watch:css と watch:js 部分は glob により watch:* という書き方も可能。並列実行であれば scripts 間の依存もないため、この方法を採用しやすい。よって serverwatch:server にリネームし、まとめて定義。

{
  "scripts": {
    "watch:server": "browser-sync start --server src",
    "watch:js": "watchify -v -t babelify ./src/js/App.js -o ./src/bundle.js -d",
    "watch:css": "watch \"npm run build:css\" ./src/stylus/",
    "watch": "npm-run-all -p watch:*"
  }
}

さっそく実行してみる。

$ npm start

> front-end-starter@1.0.1 start /front-end-starter
> npm run watch

> front-end-starter@1.0.1 watch /front-end-starter
> npm-run-all -p watch:css watch:js server

> front-end-starter@1.0.1 watch:css /front-end-starter
> watch "npm run build:css" ./src/stylus/

> front-end-starter@1.0.1 watch:js /front-end-starter
> watchify -v -t babelify ./src/js/App.js -o ./src/bundle.js -d

> Watching ./src/stylus/

> front-end-starter@1.0.1 build:css /front-end-starter
> stylus -c ./src/stylus/App.styl -o ./src/bundle.css -m

  generated ./src/bundle.css.map
  compiled ./src/bundle.css
10290 bytes written to exorcist ./src/bundle.js.map > ./src/bundle.js (1.02 seconds)

うまく並列実行されているようだ。JavaScript や Stylus ファイルを編集して保存すると、ファイル監視による自動コンパイルが走ることも確認できた。しかし Ctrl + C でコマンドを中断すると以下のエラーが表示され、停止に失敗するようだ。

$ ERROR: watch:js: None-Zero Exit(null);

エラー内容を見るに、npm-run-all はプロセスの終了コードをチェックしていて、watchify がそれを返さないことが問題なのだと思われる。シェルによる連結の場合は & による並列実行なら終了コードを無視するので、もしこれが原因なら、そういうモードに対応していただけると回避できるかもしれない。

停止に失敗した後、もう一度 Ctrl + C を実行すればよいのだが不便である。そのためwatch:js への採用は断念する。

release

release に関しては npm-run-all がピタリとハマる。本領発揮である。

{
  "scripts": {
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/bundle.css",
    "release:js": "browserify -t babelify ./src/js/App.js | uglifyjs > ./dist/bundle.js",
    "release:clean": "trash ./dist",
    "release:mkdir": "mkdirp ./dist",
    "release:copyfiles": "copyfiles -f ./src/*.html ./dist",
    "release:copydirs": "ncp ./src/fonts ./dist/fonts",
    "release": "npm-run-all -s release:mkdir release:clean release:mkdir  release:copyfiles release:copydirs -p release:css release:js"
  }
}

コマンドの実行順を保証したいなら -s、必要なければ -p の後に指定する。引数は左から順に評価されるため release では先に順番保証の必要な scripts でリリース用イメージ出力ディレクトリ構築を直列に、その後 JavaScript と CSS のコンパイルを並列実行させている。

実行管理を npm-run-all へ任せることで release:copy は不要となったため廃止。

concurrently

scripts の実行管理は npm-run-all に統一したいのだが、現時点では watchify の問題があるため、別の手段を講じる必要がある。今回は元記事の別コメントにある pandawing/awesome-nodejs-cross-platform-cli で紹介されてた concurrently を試してみる。

{
  "scripts": {
    "watch:server": "browser-sync start --server src",
    "watch:js": "watchify -v -t babelify ./src/js/App.js -o ./src/bundle.js -d",
    "watch:css": "watch \"npm run build:css\" ./src/stylus/",
    "watch": "concurrent \"npm run watch:css\" \"npm run watch:js\" \"npm run watch:server\""
  }
}

concurrently はダブルクォートされたコマンドを並列実行する。npm-run-all に比べると機能は少ないけれど、今回は連結と並列実行だけ実現できればよいため、気にしない。

concurrently 版の watch を実行してみたところ、npm-run-all と同様の並列実行が成功することを確認できた。また、Ctrl + C による中断も正常におこなわれる。もちろん Windows 上でもきちんと動作する。

まとめ

npm-run-all は高機能だけど watchify 中断でエラーになる問題がある。そのため、この箇所に限定して concurrently を採用し、それ以外は npm-run-all という感じで運用するのがよさそう。

これで元記事の Windows 問題は watchify の -o におけるパイプ機能だけになった。watchify に振り回されてる感あって辛い。とはいえ wachify は Web フロントエンド開発において有用なため、無視するわけにもゆかず悩ましい。

こうした問題を気にせず開発したい方には gulp ありの Web フロントエンド開発を勧める。gulp を利用しても、それを scripts 経由で実行するように設計しておけば、gulp をやめる時がきたとしても困らないはず。2015/8 時点で Windows 対応を考慮するなら、この方法で管理するのが妥協点になるだろう。

npm-run-all 相当の機能が npm-scripts 標準になってほしい。パッケージ依存とタスク ランナーを pacage.json だけで管理したいという需要は結構あるはず。その場合、シェル機能を利用せずに npm-scripts を連結する機能は必須といえる。標準サポートされるなら npm-run-all 風に npm run -p command1 command2 みたいに run を拡張する感じだと嬉しい。

タスク ランナーまで package.json だと設定を詰め込み過ぎではないか?という指摘もありそうだけど、ひとたび定義してしまえば滅多に変更しないだろうし、私は気にならない。

これは gulpfile.js も同様で、npm やタスク ランナー設定を頻繁に変えているとしたら設計に問題がある。この種の設定はプロジェクトの初期段階で確定するものであり、そうすると自然に package.json や gulpfile.js の更新頻度は低くなるはず。

最後に今回の変更を反映したサンプル プロジェクトを公開しておく。

冒頭にも書いたとおり、npm-run-all v1.2.8 では watchify の終了問題が解決されているため、サンプルもそれを利用したものへ変更した。バージョンも v1.0.3 としている。


COMMENTS

  • 2015年8月17日 9:04 AM 返信

    記事 ありがとうございます。
    npm-run-all の問題は調査してみますね。

    ほんと、標準に欲しいです…

REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です