アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

npm-run-all と concurrently を試す

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 Maps ファイル出力を諦めて埋込み式にしている。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:csswatch: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 標準になってほしい。パッケージ依存とタスク ランナーを package.json だけで管理したい需要は結構あるはず。その場合はシェル機能へ極力依存せず npm-scripts を連結する機能は必須だろう。標準サポートされるなら npm-run-all 風に npm run -p command1 command2 みたいに run を拡張する感じだと嬉しい。

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

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

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

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

Comments from WordPress

  • mysticatea mysticatea 2015-08-17T00:04:52Z

    記事 ありがとうございます。

    npm-run-all の問題は調査してみますね。

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