アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

npm-scripts で Web フロントエンド開発を管理する

gulp なしの Web フロントエンド開発から 1 年あまり。その間、特に問題もなく npm-scripts で Web フロントエンド開発を管理できているので、この間に得られた運用知見や所感などをまとめてみる。

npm-scrips とは?

最近の Web フロントエンド開発では AltJS/AltCSSのビルドやリリース用イメージ作成などに Node.js + npm を利用することが一般化してきた。そのためプロジェクトは package.json で管理することになる。package.json の提供する代表的な機能として

  • プロジェクト情報の定義
    • プロジェクトの成果物を npm として配布するための情報
    • プロジェクト名、バージョン、作者などのメタデータを定義する
  • 依存モジュール管理
    • プロジェクトが依存する npm とバージョンを管理する
    • この情報へ基づき npm install コマンドにより npm を一括で導入・更新できる
  • タスク管理
    • ビルドやユニットテストの実行などをタスクとして定義できる
    • タスクは Shell スクリプトとして記述
    • 定義されたタスクは Teminal から npm run TASKNAME コマンドで実行可能

などがある。これらのうちタスク管理機能を npm-scripts という。例えば

{
  "scripts": {
    "start": "npm run watch",
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "app": "electron --debug=5858 src/"
  }
}

のように定義。このようにタスクを定義して実行する仕組みをタスク ランナーと呼ぶこともある。npm-scripts 以外だと

あたりが普及している。webpack は Bundler だが設定を JavaScript で記述する関係上、タスク ランナー的に利用されることもある感じ。いまは Grunt が廃れ gulp が全盛、webpack は単体もしくは gulp と組み合わせて使われることが多い感じ。

なぜ npm-scripts か?

タスクランナーとして gulp や webpack があるのに npm-scripts を選ぶ理由としては

などで指摘されている問題を避けるため。特にプラグインの寿命は実際に遭遇したこともあって、かなり問題視している。次項で解説するが npm-scripts の扱いにくさを解決する npm によって私の用途では実用十分になったというのもある。その他、

  • 基本的に package.json だけで設定と処理が完結する
  • タスクランナー独自のルールを学習しなくて済む

などが気に入って採用している。

npm-scripts の問題とその対応

よいことずくめに感じられる npm-scripts だが問題もある。

  • 可読性
    • Node として処理を記述する gulp などに比べて単一行 Shell スクリプトは読みにくい
    • CLI オプションを変更するときに対象位置を見つけにくい
    • 前向きに評価するなら必要な情報が一行に集約されているとも言えるのだが...
  • 環境依存
    • Shell スクリプトなので実行環境によっては使えない機能がある
    • macOS や Linux などの UNIX 系と Windows は異なる
    • 複数コマンドの連結方法などは Shell 依存である
  • 変数
    • 同じ意味と値を複数箇所へ指定する場合、変数がほしくなる
    • 一応、標準で npm_package_confignpm_config が提供されている
    • ただしこれらの参照記法は Shell に依存する
    • そのためクロスプラットフォームに記述できない

まず可読性だが、これは仕方ない。どうしても CLI に馴染めないならタスク処理を Node モジュールとして用意してから

{
  "scripts": {
    "build": "node ./scripts/build.js"
  }
}

のように Node として実行する方法もあり。package.json で完結しなくなるけど、特定のタスクランナーに依存することを避けられるメリットは残る。Node モジュールなので残りの問題も同時に解決される。

npm-scripts だけでゆく場合は環境依存変数を緩和してくれる npm を利用するとよい。npm-scripts で使える便利モジュールたち - Qiita が参考になる。この記事と被るものもあるけど私の利用している npm を以下に紹介してゆく。

npm-run-all

npm-run-all は npm-scripts に定義された複数のタスクをまとめて実行してくれる。実行形式も同期・非同期から選択可能。厳密な順番が必要なら同期、そうでないなら非同期にするとよい。例えば Web アプリのリリース用イメージを作成する場合、

  1. イメージ作成用フォルダ dist を用意
  2. HTML や画像などの静的ファイルを dist へコピー
  3. AltJS をコンパイルして dist に出力
  4. AltCSS をコンパイルして dist に出力

のような感じで処理することになるだろう。これらの内、1 と 2 は依存していて 3 以降は 1 より後であればよい。これを踏まえて npm-run-all を利用すると

{
  "scripts": {
    "release:clean": "リリース用フォルダの準備",
    "release:copy": "静的ファイルをコピー",
    "release:js": "JavaScript をリリース用にコンパイル",
    "release:css": "CSS をリリース用にコンパイル",
    "release": "npm-run-all -s release:clean release:copy -p release:js release:css"
  }
}

のような感じ。-s に続けて記述したものは同期、-p なら非同期に実行。Shell スクリプトでコマンドを連結する場合 UNIX 系と Windows で記法が異なるけれど npm-run-all はそれ自体が連結を担当してくれるので記法の問題も解決される。

cpx、mkdirp、rimraf

これらはファイルとフォルダ操作系の npm。

cpx はフォルダ構造を維持したままファイルをコピーするためのツール。gulp だと標準で提供される機能だが npm-scripts の場合は Shell 依存の cp コマンドなどを利用しないと実現できない。そのためクロス プラットフォームを目指すなら cpx が役にたつ。特定の拡張子を持つものをコピーするなら

$ cpx ./src/**/{*.js,*.css} ./dist

という感じで指定。逆に特定の拡張子を除外したコピーの場合は

$ cpx ./src/**/!(*.js|*.css) ./dist

のようにする。工夫すればワンライナーでも実用十分なコピー処理を書けて便利。

mkdirp は指定された階層構造も込みでフォルダを作成してくれる。便利だが cpx はコピー先フォルダも作成してくれるので最近は使っていない。cpx の対象とならないフォルダが必要になったら利用するかも。

$ mkdirp ./dist/js ./dist/css

rimraf はフォルダを削除する。UNIX 系の rm -rf に相当し、中身の入っているフォルダも削除可能。

$ rimraf ./dist"

リリース用タスクを繰り返し実行する時、出力先フォルダに前回の結果が残っていると新旧ファイルが混ざって問題になる。これを防ぐため先に rimraf でフォルダを消してから cpx や mkdirp を実行することでフォルダがクリーンなことを保証できる。

cross-conf-env

npm-scritps で変数を利用するためには npm_package_confignpm_config を利用するのだが、これらを参照するときの記法は実行環境に依存する。cross-conf-env はこれをクロスプラットフォームに記述可能とするもの。詳しくは以下を参照のこと。

作者は私。Electron アプリ開発時に npm-scripts を複数プロジェクトで流用したくなり作成。akabekobeko/examples-electron ではビルドに使用する Electron のバージョンやアプリ名などを変数化している。

package.json のルートに定義された version フィールドなんかも参照できるのでリリース用イメージを ZIP するときのファイル名へそれを挿入、なんてことも可能。akabekobeko/redmine-theme-minimalflat2 で実際にそのようなタスクを組んでいる。

npm-scripts まめちしき

npm-scripts を運用するうえで知っておくと便利な知識をまとめる。

npm run と既定タスク

npm-scripts に定義されたタスクを CLI から呼び出す場合は

$ npm run task

とする。npm-scripts 内から別タスクを呼び出すときも同様に

{
  "scripts": {
    "task:A": "command -option",
    "task:B": "npm run task:A"
  }
}

とすればよい。これを利用するとタスクからタスクを再利用できる。starttest のように標準で意味付けされたものは npm runrun を省略して npm start のように実行可能。詳しくは scripts を参照のこと。

npm のパス

npm-scripts について解説している記事をみると、たまに

{
  "scripts": {
    "task": "$(npm bin)/command -option"
  }
}

としていたりする。これはプロジェクトのローカルにインストールされた npm を直に呼び出すとき node_modules/.bin を表す。npm-scripts であれば dependencies に登録された npm のパスが通っているので省略可能。

{
  "scripts": {
    "task": "command -option"
  }
}

npm が CLI として公開している名前を直に指定するだけで呼び出せる。なお CLI 名は必ずしも npm の名称と一致するわけではない。例えば uglify-js の CLI は uglifyjs だし npm-run-all のように複数の CLI 名を持つこともある。そのため必ず npm の README や docs から CLI リファレンスを調べる習慣をつけよう。

タスク内の引用符

npm の CLI オプションに引用符が必要だとする。package.json は JSON で npm-scripts はフィールドの文字列値となるから引用符がダブル クォーテーションならバックスラッシュでエスケープしておく。

{
  "scripts": {
    "task": "command --opt=\"options\""
  }
}

UNIX 系であればエスケープせずにシングル クォーテーションでも動作するが Windows 環境だとエラーになるので注意する。どちらを採用しても動作は変わらないから、より汎用的なエスケープによる方法をオススメする。

実践!Web フロントエンド環境構築 2016/10 版

実際に npm-scripts を利用した Web フロントエンド開発用のタスク例をまとめる。内容としては本記事の冒頭にあげた「gulp なしの Web フロントエンド開発」の 2016/10 版となる。完全なプロジェクトの構成は examples-web-app/front-end-starter を参照のこと。

Babel などの仕組みが変更されたときや気が向いたときに npm を最新にするなどして、なるべく現代的であるように運用している。特定の View 系ライブラリや Flux などの依存も避けているため、本記事の知識で理解可能な作りになっているはず。

設計方針

はじめに環境の設計方針をまとめる。

  • 環境はプロジェクトのローカルで完結させる
    • 基本的に最新 Node.js、package.json、ローカル npm だけ使用する
    • 例えば Node 以外で「〜を入れて」という作業は不要
  • JavaScript コンパイルとファイル監視に対応する
    • AltJS には Babel を採用
    • latest プラグインにより常に最新の ECMAScript でコーディング可能
  • CSS コンパイルとファイル監視に対応する
    • AltCSS には Stylus を採用
  • ユニット テストに対応する
    • mocha と power-assert を採用
    • テスト自体も最新の ECMAScript で記述可能とする
  • コード ドキュメントに対応する
    • ESDoc を採用
  • クロスプラットフォーム対応
    • UNIX 系と Windows 環境で動作する
    • 前述の npm-scripts 向け npm 群により実現

AltJS だけ tsify にするとか webpack 管理にしたり AltCSS を SCSS や PostCSS に変更してもよい。npm-scripts のタスクとして呼び出せるならば、これらは可換である。

プロジェクトのファイル構成と npm-scripts の全体像

プロジェクトのファイル構成は以下となる。ライセンス情報や README などは必須ではないため除外。

.
├── esdoc.json
├── package.json
├── dist/
├── node_modules/
├── src/
│   ├── assets/
│   ├── js/
│   └── stylus/
└── test/

各ファイル、フォルダの役割をまとめる。

名前 内容
esdoc.json ESDoc 用の設定ファイル。コード ドキュメントを出力するために必要。
package.json プロジェクト情報や開発用タスクを管理するためのファイル。
dist/ リリース用イメージの出力先となるフォルダ。デプロイ対象となる。
node_modules/ インストールされた npm を格納するフォルダ。
src/ 開発用リソースを格納するフォルダ。
src/assets/ HTML、画像、Web Fonts などの静的リソースを格納するフォルダ。
src/js/ JavaScript 関連を格納するフォルダ。コンパイル結果は src/assets/ または dist/ に出力する。
src/stylus/ Stylus 関連を格納するフォルダ。コンパイル結果は src/assets/ または dist/ に出力する。
test/ ユニット テスト関連を格納するフォルダ。

静的リソースを src/ 直下ではなく src/assets/ としているのはリリース用イメージを生成するときのコピー指定を簡略化するため。このあたりは後ほど詳しく解説する。

以降では npm-scripts や設定を小分けに解説するが、それだと分かりにくいかもしれない。よって nameversion などの基本情報を除く、Web フロントエンド開発に関わるものの全体像も掲載しておく。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "browserify": {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "start": "npm run watch",
    "esdoc": "esdoc -c esdoc.json",
    "build:css": "stylus -c --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "build:js": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "build": "npm-run-all -p build:css build:js",
    "watch:css": "stylus -c -w --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "watch:js": "watchify ./src/js/App.js -v -o \"exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js\" -d",
    "watch:server": "browser-sync start --server ./ --startPath src/assets/",
    "watch": "npm-run-all -p watch:css watch:js watch:server",
    "release:css": "stylus -c --include-css ./src/stylus/App.styl -o ./dist/bundle.css",
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs -c warnings=false -m > ./dist/bundle.js",
    "release:clean": "rimraf ./dist",
    "release:copy": "cpx \"./src/assets/**/!(*.js|*.css|*.map)\" ./dist",
    "release": "npm-run-all -s release:clean release:copy -p release:css release:js"
  },
  "dependencies": {
    "normalize.css": "^5.0.0"
  },
  "devDependencies": {
    "babel-preset-latest": "^6.16.0",
    "babel-preset-power-assert": "^1.0.0",
    "babel-register": "^6.16.3",
    "babelify": "^7.3.0",
    "browser-sync": "^2.17.0",
    "browserify": "^13.1.0",
    "cpx": "^1.5.0",
    "cross-env": "^3.1.1",
    "esdoc": "^0.4.8",
    "exorcist": "^0.4.0",
    "mocha": "^3.1.0",
    "npm-run-all": "^3.1.0",
    "power-assert": "^1.4.1",
    "rimraf": "^2.5.4",
    "stylus": "^0.54.5",
    "uglify-js": "^2.7.3",
    "watchify": "^3.7.0"
  }
}

dependenciesdevDependencies は 2016/10/7 時点で最新のもの。

JavaScript コンパイルとファイル監視

JavaScript は ES2015 以降の最新規格で記述可能して ES5 に変換する。

2016/10 時点でも ES2015 へ 100% 対応した Web ブラウザーは WebKit とそれを使用する Safari 10 ぐらいである。そのため ES2015 であってもしばらくは変換が必要。以降の規格も考慮すると今後も変換は前提となるだろう。

コンパイルには npm-scripts だけでなく ES5 変換で使用する Babel や複数 JavaScript を bundle (結合) する Browserify の設定も必要。Babel の設定は .babelrc というファイルへ記述するのが一般的。しかし私は設定を package.json へ集約するため、意図的に babel フィールドで指定している。

Browserify は変換に噛ませるプラグイン指定を transform にくくり出せる程度なので npm-scripts 側へ -t [ babelify ] と CLI オプションで指定するほうがよいかもしれない。タスクが長くなってもよいなら CLI オプションにしよう。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "browserify": {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "build:js": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "watch:js": "watchify ./src/js/App.js -v -o \"exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js\" -d",
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs -c warnings=false -m > ./dist/bundle.js",
  }
}

各タスクの処理内容を解説。

タスク 処理
build:js 開発用に JavaScript をコンパイルして Source Maps と一緒に src/assets/ へ JavaScript ファイルを出力する。
watch:js 変更監視つきで JavaScript をコンパイルして Source Maps 一緒に src/assets/ へ JavaScript ファイルを出力する。
release:js リリース用に JavaScript をコンパイルして dist/ へ JavaScript ファイルを出力する。

必要な npm をまとめる。

npm 機能
browserify モジュールとして定義された複数のJavaScript 依存を解決して単一ファイルへ bundle する。プラグインを指定することで bundle 前に AltJS の変換も実行可能。
watchify Browserify のファイル監視 & 差分コンパイル版。
babelify ES2015 以降の機能を使用した JavaScript を ES5 に変換する Babel 公式の Browserify 用プラグイン。
babel-preset-latest ES2015 以降の規格に準じた変換用 Babel プリセットをまとめたもの。詳しくは Latest preset を参照のこと。
cross-env リリース用コンパイル時に Node の環境変数へ NODE_ENV=production を追加して、これを判定しているデバッグ用コードを削除するために使用。
exorcist 開発用に JavaScript を変換する際、Web ブラウザの開発者ツールから元コードでデバッグするための Source Maps ファイルを生成する。Browserify/babelify の変換結果をコマンドライン連結で受け取り Source Maps と JavaScript ファイルを出力する。
uglify-js リリース用コンパイル時、コード圧縮、使用されていない変数や到達不能コードの削除、引数や関数名の短縮などを実行する。これも exorcist と同様にコマンドライン連結で受け取った JavaScript を加工してファイルに出力する。

タスクとしては参照しないが IE11 環境で Promise などを利用するなら babel-polyfill も必要。これは言語機能ではなく API なので latest にも含まれない。そのため個別に追加してコード全体の冒頭へ import することになる。

Browserify で webpack のように複数の JavaScript を生成したい場合は Browserify の require オプションでモジュールを外部公開するを参照のこと。npm-scripts のタスクも生成したい数だけ定義して、まとめて呼び出せばよい。

CSS コンパイルとファイル監視

CSS は Stylus で書いて CSS3 に変換。

{
  "scripts": {
    "build:css": "stylus -c --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "watch:css": "stylus -c -w --include-css ./src/stylus/App.styl -o ./src/assets/bundle.css -m --sourcemap-base ../stylus",
    "release:css": "stylus -c --include-css ./src/stylus/App.styl -o ./dist/bundle.css"
  }
}

各タスクの処理内容を解説。

タスク 処理
build:css 開発用に Stylus ファイルをコンパイルして Source Maps と一緒に src/assets/ へ CSS ファイルを出力する。 
watch:css 開発用に Stylus ファイル変更を監視しながら Source Maps と一緒に src/assets/ へ CSS ファイルを出力する。
release:css リリース用に Stylus ファイルをコンパイルして Source Maps と一緒に src/assets/ へ CSS ファイルを出力する。

必要な npm をまとめる。

npm 機能
stylus Stylus ファイルを CSS にコンパイル。Sourcde Maps 生成、圧縮、ファイル監視機能も搭載している。

最近の AltCSS では JavaScript における Babel のように将来標準を先取りする目的で postcss/postcss が台頭。その CLI 版である postcss-cli も Stylus のように単体で開発に必要な機能を網羅している。そのため将来は PostCSS へ移行するかもしれない。

Web サーバー起動

Web フロントエンド部分を動作確認するとき、ローカル ファイルを直に Web ブラウザで表示するとセキュリティ上の問題を引き起こすことがある。これを防ぐため Chrome ではローカル ページ上では Ajax や Web Storage などの利用を抑止する対策が取られている。

しかし開発環境であることを理解して表示する分には、この制限は不便である。そのためローカルに簡易 Web サーバーを起動して Web ページをホストさせる。

{
  "scripts": {
    "watch:server": "browser-sync start --server ./ --startPath src/assets/"
  }
}

各タスクの処理内容を解説。

タスク 処理
watch:server 指定されたフォルダをルートにして Web サーバー起動、OS 標準の Web ブラウザで表示する。この例ではプロジェクトのルートを指定、表示する初期ページは src/assets/ 内の index.html になる。

必要な npm をまとめる。

npm 機能
browser-sync Web サーバーと Web ブラウザ表示を担当。

browser-sync は非常に多機能。npm-scritps から利用するための CLI については Browsersync Command Line Usage を参照のこと。特に指定されたファイルの変更を検出して Web ブラウザーを自動更新する機能は便利だ。しかし私は変更前の状態を確認しつつ任意のタイミングで手動更新するほうが好みなので使用していない。

自動更新を有効にしたいなら browser-sync の CLI オプションに reload --files=\"src/**/*\" を追加。すると src/ 配下のファイルが更新されるたびに Web ブラウザに読み込まれたページを自動更新してくれる。

browser-sync の CLI を実行すると Terminal に localhost と IP アドレスの 2 種類、URL が表示される。実行マシンの Web ブラウザーには前者、同一ネットワーク上の別 PC やモバイル端末から動作確認したいなら後者へアクセスすればよい。

レスポンシブ デザインを試すだけなら PC 上でもよいが、タッチ操作なども含めた実際の操作感はモバイル端末の実機でチェックしたほうがよい。またオフィス内で他社に Web ページを公開するときもにもこの機能は便利だ。

ユニット テスト

ユニット テストには mocha と power-assert を採用。テストも最新の ECMASCript で記述可能とする。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js"
  }
}

テスト コードは test/ 配下に置きテスト対象となる機能を含む JavaScript ファイルの拡張子を .test.js に変えたものとする。このようにすることで

  • テスト対象とテストの対応が分かりやすい
  • Atom などのエディタでテスト対象とテストを並べても名前が衝突しない
  • test/ 配下にテストを含まない補助コードを置いても、それらとテストを区別できる

というメリットがある。

Babel を使用するため .babelrc か package.json の babel フィールドに設定が必要。これは JavaScript コンパイルと共有されるので latest による変換は共通、power-assert は env フィールドで開発時のみ有効にしておく。

こうするとリリース時のコンパイルで NODE_ENV=production が指定されていたら power-assert 処理を除外してくれる。Web フロントエンド開発なのでアプリ側コードに Node の assert が露出することはないけど安全のため指定しておいたほうがよい。

npm を自作するとき、この設定を流用できる利点もある。npm は Node 環境で実行されるため普通に assert を呼べるので。

各タスクの処理内容を解説。

タスク 処理
test ユニット テストを実行する。

必要な npm をまとめる。

npm 機能
mocha テスト コードを実行するための環境。
babel-preset-latest ES2015 以降の規格に準じた変換用 Babel プリセットをまとめたもの。
babel-preset-power-assert 現在の Babel で power-assert を利用するための Babel prest。
babel-register Babel の JavaScript 変換処理を捕捉して間に処理をはさみこむためのツール。power-assert による Node 標準 assert の置換に使用。

Babel 本体はどこへ?と疑問に思われるかもしれないが、アプリ開発で使用する babelify の依存で確実にインストールされるため問題ない。ユニット テストは基本的に DOM の絡まないものを対象とする。もし DOM 操作を含むコードをテストしたいなら jsdom などをオプションで追加してもよい。

mocha はひとつのテストにつき標準で 2 秒の制限時間を設定。jsdom は初期化に時間がかかるため、この制限に引っかかって警告される可能性がある。その場合は mocha の CLI オプションへ --timeout 50000 のように大きめの制限時間を指定することで回避可能。時間の単位はミリ秒だからこの例だと 5 秒。

注意点がある。power-assert の置換対象は assert なので

import assert from 'assert';

ならよいけど

import Assert from 'assert'

にすると Assert は置換されず、Node 標準の assert になってしまう。そのため import または require 先は必ず assert にすること。

コードド キュメント

最新の ECMAScript で記述されたアプリ実装コードとコメントからコード ドキュメントを生成する。その際、コメント記述のカバレッジや機能とユニット テストの関連付けもおこないたいので ESDoc を利用。

{
  "scripts": {
    "esdoc": "esdoc -c esdoc.json"
  }
}

コード ドキュメントがあると第三者に設計や実装を説明する時のよい参考資料になる。また、ドキュメント生成を前提とすることで読まれるコメントを書こうと意識するようにもなるだろう。コードに最も近い場所へ記述される仕様ともいえる。あるクラスや関数が実現したい機能やインターフェースについて書くようにすると、それは自然と現状にそった自己言及的なドキュメントになるはず。

各タスクの処理内容を解説。

タスク 処理
esdoc アプリの実装コードとテストを解析して HTML の資料を生成する。出力先は esdoc/

必要な npm をまとめる。

npm 機能
esdoc コード ドキュメント生成ツール。

ESDoc の設定は CLI オプションや package.json ではなく esdoc.json に記述する。このファイルはプロジェクト全般の設定に属するため、package.json と同じ階層に保存するとよい。

設定内容については ESDoc - A Documentation Generator For JavaScript(ES6) を参照のこと。

{
  "source": "./src/js",
  "destination": "./esdoc",
  "test": {
    "type": "mocha",
    "source": "./test"
  }
}

ESDoc が出力する HTML はこんな感じESDoc Hosting Service というサービス (beta 版) も運用されている。ここに登録されたプロジェクトの GitHub 上のリポジトリから esdoc.json を検出すると HTML 生成して Web へ公開してくれる。ありがたい。

README.md などにバッジを貼り付けてホストされた HTML へリンクできるので GitHub でリポジトリ運用している OSS なプロジェクトなら採用をオススメする。

複数のタスクを組み合わせる

JavaScript や CSS 用のタスクを定義したら、これらを組み合わせたくなる。例えば開発時はファイルの自動監視とコンパイルを走らせつつ動作確認用に Web サーバーを起動したい。このような連結は前述の npm-run-all を利用することでクロスプラットフォームにできる。

{
  "scripts": {
    "build": "npm-run-all -p build:css build:js",
    "watch": "npm-run-all -p watch:css watch:js watch:server",
    "release": "npm-run-all -s release:clean release:copy -p release:css release:js"
  }
}

npm-run-all を前提に完結した小さなタスクを定義してから組み合わせて実行すると管理しやすくなる。組み合わせられるものはタスク名で分類を明示するとよい。私は

タスク 処理
build:XXXX 開発ビルド。XXXX には js や css など処理対象を付与する。
build build:XXXX を組み合わせて実行するタスク。
watch:XXXX ファイル変更の監視つき開発ビルドや Web サーバー起動など。XXXX には js や css など処理対象を付与する。
watch watch:XXXX を組み合わせて実行するタスク。
release:XXXX リリース用ビルド。XXXX には js や css など処理対象を付与する。
release release:XXXX を組み合わせて実行するタスク。

という感じで命名している。

リリース用イメージ生成

Web フロントエンド開発の成果物をリリースするためのイメージを生成する。

{
  "scripts": {
    "release:clean": "rimraf ./dist",
    "release:copy": "cpx \"./src/assets/**/!(*.js|*.css|*.map)\" ./dist",
    "release": "npm-run-all -s release:clean release:copy -p release:css release:js"
  }
}

release:jsrelease:css はそれぞれの項に解説したので割愛。各タスクの処理内容を解説。

タスク 処理
release:clean リリース用イメージの生成先となる dist/ フォルダを削除。前回の生成結果が残っていないことを保証する。
release:copy src/assets/ 内の静的リリースを対象にフォルダ階層を維持して dist/ にコピー。
release リリース用イメージ生成タスクを連結実行。生成先フォルダの削除と静的リソースのコピーを直列実行することで基本的な dist/ を準備、その後にここへリリース用の JavaScript と CSS を出力する。

必要な npm をまとめる。

npm 機能
rimraf 指定されたフォルダを中身ごと削除する。
cpx glob 形式で指定された条件にあったファイルやフォルダを、所定のフォルダへコピーする。
npm-run-all npm-scripts の複数タスクを連結して直列または並列実行する。

release:copy についての補足。「gulp なしの〜」を書いたときのコピー関連処理は

{
  "scripts": {
    "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",
  }
}

のようになっていて非常に複雑だった。あの記事のはてブでもやり過ぎ感あると指摘されていた。これを解決するために

  • 静的リソースは src/ ではなく src/assets/ に置く
    • 静的リソースと動的リソース系の src/js/src/stylus/ を区別しやすくなる
    • src/assets/ に格納されているものは、そのままリリース用にコピー可能とする
    • src/assets/ は開発用ビルドで生成された JavaScript、CSS、Source Maps が動作確認のため出力される
  • cpx でブラック リスト式コピー
    • src/assets/ に出力された動的リソースをブラック リスト化して、それ以外をコピーする
    • 動的リソースはそれらのリリース用ビルドで dist/ へ出力するためコピー不要
    • 不要ファイルのブラック リスト化は glob の否定機能を利用する
    • cpx はコピー先フォルダを生成してくれるので mkdir も不要になる

上記で対応。npm-scripts が大幅に簡素化された。運用としても静的リリースの置き場所が明確なので管理しやすい。

タスクを利用した開発スタイル

はじめにプロジェクトを構築する。Terminal は Windows の場合 cmd.exePowerShell などになる。

  1. プロジェクト開発フォルダを用意
  2. 開発フォルダのルートに package.json を作成、または既存のものを流用
  3. Terminal から npm i コマンドを実行して npm をインストール
  4. src/ 内に JavaScript、CSS、HTML を実装してゆく
  5. あわせて test/ にテストを書く

開発時に実行すること。

  1. Terminal から npm start コマンドを実行、JavaScript と CSS のファイル監視と Web サーバー起動
    1. この記事の設定なら http://localhost:3000/src/assets/ が Web ブラウザーに表示される
    2. Terminal 上に IP アドレス版の URL も表示される
    3. 同一ネットワーク上の PC やスマートフォンで表示する場合は IP アドレス版の URL で OK
  2. JavaScript や Stylus ファイルを編集して保存
    1. Babel や Stylus の自動コンパイルが実行される
    2. Terminal にコンパイル終了が表示されるので Web ブラウザをリロードするときの目安になる
    3. 必要ならテストを書いて別 Terminal または別タブから npm test でチェック
  3. Web ブラウザをリロードして変更をチェック
    1. http://localhost:3000/src/assets/ をリロード
    2. browser-syncreload オプションで実行しているなら自動化される
  4. 2 〜 3 を繰り返し、終了したくなったら Ctrl + C で中断

リリース時に実行することは以下。

  1. Terminal から npm run release コマンドを実行
  2. Terminal をチェックして全タスクが終了するまで見守る
  3. 全タスクが終了したら dist/ の中身をリリース

JenkinsTravis CI などと組み合わせてもよい。例えば全テストを通過したときだけリリース (デプロイ) するとか。

まとめ

ここ 1 年ほどの npm-scripts 運用知見をまとめてみた。

クロスプラットフォーム対応も済んでおり、この記事にまとめた内容とほぼ同等の構成で業務プロジェクトも運用している。静的リソースを src/assets/ に集約する設計はデザイナーや HTML コーダーにも好評であった。

Browserify、Babel、Stylus が存続する限り今回の構成で完成形と考えている。もしこれらがなくなったとしても依存は npm だけなので十分に可換。例えば JavaScript だけ webpack、CSS を PostCSS というのもありだ。

最後に本記事で紹介した npm-run-allcpx の作者であり、npm-scripts 運用についてのアドバイスもいただいた mysticatea (Toru Nagashima) さんに感謝したい。いつもありがとうございます。

Copyright © 2009 - 2023 akabeko.me All Rights Reserved.