npm 開発で脱 Babel してみる

2017年1月25日 0 開発 , , ,

自作 npm の開発で脱 Babel したときの対応と問題点まとめ。

  • 2017/1/31 訂正
    power-assert の作者、t_wada さんより power-assert は babel-register を通すなどしないと assert 置換が働かず素の assert になってしまうという指摘があったのでユニットテスト関連の記述を訂正

脱 Babel を決めた背景

私はいくつか自作 npm を公開している。これらは ES2015 以降の機能と構文を利用して npm publish の際に Babel で ES5 相当へ transpile している。この運用で特に問題も起きていない。またプリセットに babel-preset-latest を採用することで ES 関係の規格追従を Babel 任せにできる安心感もあり、ずっとこのままでいいと思っていた。

ある日、職場で Node アプリを開発している人から「Babel 依存は怖くないですか?」という質問があった。Babel にバグがあったら調査や修正は困難だし、使わなくてよいならばそうするに越したことはないのでは?と。

これまで C++、C#、Java などコンパイル前提の言語で開発した経験から、よほどのことがない限りコンパイル結果は信頼に足ると判断していた。また、コンパイルされたマシン語や中間言語を人間が直に記述するのは非常にキツイ。そのため脱コンパイラーという選択肢は現実的ではないと認識している。

一方、JavaScript における transpile は高級言語どうしの変換となる。その気になれば人間が書けるのだ。

transpiler への慣れから、この事実をすっかり忘れていた。Node であれば Web フロントエンドと異なり動作環境の分岐は少なく、package.json の engines にて対象環境を限定することも可能である。ならば新しい Node を前提として脱 Babel を検討してみるのもよさそうと考えはじめた。

そんな折、npm-run-all が v4.0.0 で Babel による transpile を廃止。Node は v4 時点で ES Modules を除く大半の ES2015 機能が実装されているため、この範囲で足りるなら transpile せずにそのままリリース可能だ。その前例として普段利用しているツールが脱 Babel したのはインパクトある。

これらを踏まえ、まずは自作 npm のうちダウンロード数の少なくニッチな wpxml2md から脱 Babel を試してみることにした。

脱 Babel への道のり

脱 Babel 対応で実施したことを書く。

Node 環境の明示的な指定

脱 Babel における前提条件として動作環境とする Node のバージョンを決める。npm-run-all は Node v4 を下限としているようだが、wpxml2md では v6 としておく。

開発と検証コストを考慮して自作 npm の動作環境は「最新 + 最新 LTS」としている。2017/1 時点の最新 Node は v7 系、LTS は v6 と v4 があるため対象は「v7 + v6」となる。これまでは Babel による transpile で v4 以下でも動作していたのだが、これを廃止することで明示的な下限の指定が必要となった。これは package.json の engines プロパティに記述する。

{
  "engines": {
    "node": ">= 6"
  }
}

ちなみに

node v6, v7

というバッヂを用意していて前から README へ掲載していた。今回の対応により、ようやくこれが本来の意味をあらわすようになった。

Babel の transpile を廃止

これまでは Babel の transpile を前提として以下のような Babel 設定と npm-scripts を利用していた。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register test/**/*.test.js",
    "watch": "babel src --out-dir ./ --watch",
    "start": "npm run watch",
    "build": "babel src --out-dir ./",
    "prepublish": "npm run build"
  }
}
script 内容
test 予約された npm run のタスク。 mocha と power-assert によるユニット テスト。
watch 開発用。ファイル監視による自動 transpile を実行。
start 予約された npm run のタスク。watch を呼び出すだけ。
build リリース用。現時点のソース コードで transpile を実行。
prepublish 予約された npm publish 時に呼び出されるタスク。build を呼び出しているため npm として公開されるイメージは transpile されたものになる。なお prepublish は現在 deprecated になっていて prepare へ修正すべきなのだが直し忘れていた。

これが脱 Babel によりこうなる。

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

transpile 不要となるため関連するタスクが消え、ユニット テストだけが残った。ただし power-assert を利用しているなら標準 assert を置換するための transpile が必要なので、このための設定は維持する。

env.development.presetspower-assert だけ指定することで、Babel 依存がユニット テストに限定されていることが明示されるだろう。必要な Babel 関連の npm も babel-registerbabel-preset-power-assert だけになる。

ES Moduels を CommonJS 化する

Node の ES Modules 対応については以下が詳しい。

なお現時点の最新 Node である v7 においても ES Modules には対応していないため、export/import は CommonJS の exports/require へ修正する必要がある。例えば

export const Options = {
};

export default class CLI {
}

const Options = {
};

class CLI {
}

module.exports = {
  Options: Options,
  CLI: CLI
};

とする。読み込む側は

import CLI from `cli.js`;
import { Options } from `cli.js`;

const CLI = require( `cli.js` ).CLI;
const Options = require( `cli.js` ).Options;

とする。ひとつのモジュールから exportdefault export で複数のインターフェースを公開していると面倒である。しかし ES Modules で書いていたなら import はソース コード冒頭に集約されているから悩む余地なく機会的な作業になる。なんなら正規表現でまとめて変換できる。

ユニット テストについても同様に対応すること。

余談。

CommonJS にした後でも将来の ES Modules 移行を容易にするため require はソース コード冒頭へ書く習慣をつけたほうがよいかもしれない。その場合、読み込み先を camelCase で命名しているとローカル変数と競合する可能性が高くなるため PascalCase にしたくなり、私はそうしている。例えば fsFs と命名している。

Node と CommonJS だとスコープを意識して require を使い分け、なるべく関数ローカルで宣言する派が多数な感じなので ES Modules 対応されたときが気になる。私のようにするか、それともよりよい慣習となるのか?実に楽しみだ。

ESDoc 対応

npm のコード ドキュメント生成に ESDoc を採用している場合、そのままでは CommonJS を解釈できないので対応が必要になる。CommonJS は

上記 issue で紹介されている esdoc-node により対応できる。ESDoc にプラグイン機能があることを初めて知った。これを指定することでコードが ESDoc に解釈される前処理を実行できるらしい。例えば esdoc-node は CommonJS を ES Modules に変換して ESDoc に渡す。

さっそく使ってみよう。まず esdoc-node をプロジェクトに追加。

$ npm install -D esdoc-node

次にこれを ESDoc 設定へ追加する。私は ESDoc の設定を package.json に定義しているので

{
  "esdoc": {
    "source": "./src",
    "destination": "./esdoc",
    "test": {
      "type": "mocha",
      "source": "./test"
    },
    "plugins": [
      { "name": "esdoc-node" }
    ]
  }
}

このようにした。設定後、ESDoc を実行して CommonJS なコードも正しく解析されることを確認。

ただしローカル実行ではなく ESDoc Hosting Service は CommonJS やプラグインに対応していないようだ。試しに wpxml2md の API Document を生成 してみたのだが、2017/1/25 時点では esdoc-node 未指定の状態と同様に CommonJS の絡むものが解釈できていない。

この点について要望 issue を登録してみたのだけど、もしプラグイン対応する場合

  • Hosting Service 側でプラグインを網羅しておき選択実行する
    • プラグインを中央管理する仕組みがないと網羅できない
    • プラグインのバージョンはどうするのか?常に最新?
  • プロジェクトの ESDoc 設定と package.json から動的にプラグインを決定する
    • プラグインのインストールはどうする?
    • ドキュメント生成とごにプラグインをインストールすると Hosting Service の負荷が大きい
    • CI 系サービスのように VM や Docker コンテナで対応するとしても負荷は大きい

といった問題が予想されるため難しそうだ。しかし要望があることだけは記録しておきたかったので issue 登録することにした。

別の方法として ESDoc 本体が esdoc-node 処理を取り込むという選択肢もある。ただ、いずれ Node が ES Modules へ正式に対応するとしたら CommonJS は過渡期の存在である。

  • そのために対応コストを割くのか?
  • ESDoc と名乗るツールとして ECMAScript とは直に関係しない CommonJS へ対応することは設計思想として望ましくないのでは?

という考えもあるだろう。どのような対応、または非対応のままになったとしても ESDoc 作者の意向を尊重したい。

ESDoc Hosting Service を利用しない場合、対象プロジェクトが GitHub で管理されているならそのリポジトリに対して GitHub Pages を使う手もある。ローカルの ESDoc + esdoc-node で出力したものを自前でアップロードするか、そういうタスクを npm-scripts に定義して CI サービス経由で生成から公開まで自動化するなどの方法が考えられる。

本記事を書いた直後に ESDoc 作者の @h13i32maru さんから見解が。

ESDoc Hosting Service 的にはセキュリティや負荷を考慮し、ESDoc 公式プラグインのみサポートしているとのこと。ただ Node の ES Modules 対応は相当に先となりそうなので、この長い過渡期に Node かつ脱 Babel したい開発者としてどうか?という悩ましい課題がある。

まとめ

一部、問題もあったが transpile 不要となったことで Node のバージョンだけを意識して開発すればよい。記述したコードはそのまま実行されることが保証される。Babel 由来の潜在的なトラブルを考えなくてよいため精神的に楽。

あと npm をユニット テストではなく普通のプログラムとして走らせて検証してみたい場合、いちいち transpile しなくて済むのも助かる。

検証はテストに定義すべきでは?という意見もあるだろうけどリポジトリの examples フォルダに npm として参照したときのサンプルを配置しているとき、その動作検証で npm publish する前の現行コードを試したくなったりする。その場合、transpile なしだとビルド系タスクを実行せず動かせてよい。

以上を踏まえ、他の npm についても気が向いたら脱 Babel してゆく予定。

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

2016年10月7日 0 開発 , , ,

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 以外だと

あたりが普及している。いまは 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 するときのファイル名に version を挿入なんてことも可能。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) さんに感謝したい。いつもありがとうございます。

examples-web-app 更新 2016/5

2016年5月25日 0 開発 , , , ,

akabekobeko/examples-web-app にある front-end-starter と front-end-starter-with-gulp を更新した。ここ最近の開発で得られた知見や方針を反映している。それらについては Twitter でもつぶやいていたのだけど、せっかくなので記事にまとめておく。

静的リソース用フォルダの変更

従来のフォルダ構成は

.
├── package.json
├── src/
│   ├── index.html
│   ├── fonts/
│   ├── js/
│   └── stylus/
└── test/

となっていた。src/ 直下と Web サイトのルートを一致させていたのだが JavaScript と Stylus の
開発用リソースと index.htmlfonts のような静的リソースを区別しにくかった。そこで構成を以下のように変更。

.
├── package.json
├── src/
│   ├── assets/
│   │   ├── fonts/
│   │   └── index.html
│   ├── js/
│   └── stylus/
└── test/

静的リソースは assets に置かれる。JavaScript や Stylus のコンパイル結果は assets に出力される。今後、例えば画像を静的リソースとして追加する場合は src 直下ではなく assets 配下に置く。静的なものと開発用フォルダを分けたことで見通しがよくなった。

browser-sync

Stylus の Source Maps 参照は元ファイルの相対パス指定が必要となる。そのため browser-sync の表示対象としたフォルダ内にそれらが含まれていなければならない。しかし assets をルートすると stylus フォルダが見えないので Source Maps を参照できなくなる。

この問題を解決するためにはルートを src に変更した場合、npm としてインストール & 参照している normalize.css が含まれない。よってプロジェクト全体のルートになる ./ を指定する。

この状態で browser-sync を起動すると Web ブラウザで初期表示される階層が ./ になるため、src/assets を表示するためには手動で URL を修正しなければならない。これは面倒なので、初期表示するページも同時に指定しておく。

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

--server に指定されたパスが Web サイトのルートになる。初期表示するページはルートからの相対パスとして --startPath へ指定すればよい。index.html はデフォルトの表示対象なので省略可能。別のページにするなら明示的に指定してもよい。

これでプロジェクト配下にある全てのファイルとフォルダを参照できるようになった。

Stylus 関連

これまで Stylus の CLI 設定は npm-scripts で以下のようにし定義ていた。

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

Source Maps における元ソースの参照を --sourcemap-root で指定していたのだが、いつからかこの方法だと Not Found になっていた。改めて Executable — Stylus を見直したら Source Maps 関連のオプションに --sourcemap-base というものがある。相対パスで指定する場合、これを利用するのが正しいので修正した。

それと Normalize.css を npm で管理して Stylus に組み込むに書いた内容を反映した npm-scripts は以下となる。

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

Source Maps の相対パスが ./stylus ではなく ../stylus になっているのは、前述の静的リソース用フォルダ変更への対応となる。

JavaScript 関連

Babel の設定を .babelrc から package.json の babel プロパティに移動した。mocha についても espower-babel から babel-preset-power-assert への移行で書いたようにしばらく mocha.opts で運用していたのだが、Babel にあわせて package.json の npm-scripts へ定義する方法に戻した。

{
  "babel": {
    "presets": [
      "es2015"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },  
  "browserify" : {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "build:js": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "watch:js": "watchify -v -t [ babelify ] ./src/js/App.js -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 > ./dist/bundle.js"
  }
}

Browserify の transform 設定も package.json の browserify.transform に定義して CLI オプションから -t [ babelify ] を削除している。しかし watchify はこれを無視するらしく、watch が落ちてしまう。コンパイルにも失敗しているようで bundle.js の実処理は空だ。仕方ないので watchify だけオプションを残している。

もうひとつ、production ビルドについて。

front-end-starter では View や Flux 系の npm は組み込まない方針だが、React などを追加した場合、そのまま require/import するとデバッグ用の処理が大量に残る。それらは

if (process.env.NODE_ENV !== 'production') {
}

のようになっているため、残ったとしてもさほど実害はない。しかしサイズは巨大だし React のリリース用イメージである react.min.js からは除去されているものだから自前で bundle する場合もそのようにしたい。

これを実現するためには Babel のビルド時に NODE_ENV=production を指定すればよい。npm-scripts で実行するなら環境変数の設定だけでなく、その記法もクロスプラットフォーム対応させたいので cross-env を利用する。前述のサンプルから当該部分だけ抜き出すと

{
  "babel": {
    // Babel 設定
  },  
  "browserify" : {
    // browserify 設定
  },
  "scripts": {
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs > ./dist/bundle.js"
  }
}

のようになる。

この話と除去の原理については browserify + npmでReactを使う場合はNODE_ENVを設定するとよい – Qiita を参照のこと。Downloads | React の Note でも production と mishoo/UglifyJS2 について言及されている。

React v15.1.0 を bundle する場合、この処理の有無でファイルサイズが 8KB ぐらい縮小された。

gulpfile を ES2015 対応させる

いまは gulp を利用していないのだけど、何気なく front-end-starter-with-gulp の npm を更新してみたら gulp-watchify が更新されていて最新 Browserify に対応しているようだったので、これも最新構成に修正してみた。

gulp v4 を目前に控えており、そちらでは gulp-load-plugins で実現していた処理が標準化されるなどタスク実装を改善するレベルの機能追加がある。それを待つつもりだったが、そう考えてから数ヶ月すぎて未だ v3.9 のままなので、現時点で可能な ES2015 対応だけ反映することにした。

しかし 2016/5/25 時点の gulpjs/gulp を babel や ES2015 で検索しても gulp/exports-as-tasks.md とか README、CHANGELOG ぐらいしか情報がない。機能としては実装されているが公式リファレンスはこれからなのだろうか。

gulpfile ES2015 とかでググると gulpfileをES2015(ES6)で書くUsing ES6 with gulp などが見つかった。後者の記事では Babel 6 以降の変更を反映しているため、主にこちらを参考にする。

ES2015 対応を試すにあたり、問題が起きたときの切り分けを簡単にするため最小のプロジェクトを用意することにした。npm init して package.json だけ存在する状態から開始する。

{
  "name": "gulp-es2015",
  "version": "1.0.0",
  "description": "gulp-es2015",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "scripts": {
    "test": ""
  }
}

はじめに必要な npm を揃える。参考記事では gulpbabel-corebabel-registerbabel-preset-es2015 を利用しているので、まとめてインストール。

$ npm i -D gulp babel-core babel-register babel-preset-es2015

なお babel-core は babel-register をインストールすると依存で入る。そのためか明示的にインストールしなくても ES2015 版の gulpfile を処理できるのだが、公式リファレンスの言及がないので参考記事に従い、すべて入れておいた。

Babel の preset 設定は package.json に定義。依存や設定は可能な限り package.json で管理する方針。

gulp の default タスクを npm-scripts から呼び出すように定義。こうすると npm start でプロジェクトのローカルにある gulp を使用するのでグローバルにインストールしなくても済む。

これらをすべて反映した状態の package.json。

{
  "name": "gulp-es2015",
  "version": "1.0.0",
  "description": "gulp-es2015",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "babel": {
    "presets": [
      "es2015"
    ]
  },
  "scripts": {
    "start": "gulp"
  },
  "devDependencies": {
    "babel-core": "^6.9.0",
    "babel-preset-es2015": "^6.9.0",
    "babel-register": "^6.9.0",
    "gulp": "^3.9.1"
  }
}

環境が整ったので gulpfile を実装する。ES2015 で書く場合はファイル名を gulpfile.babel.js にしておく。すると gulp が babel-register 経由で babel-preset-es2015 を呼び出して ES2015 で記述されたファイルをコンパイル & 実行という流れで処理される仕組みのようだ。

gulpfile.babel.js を実装。console.log するだけの簡素なタスクを定義しておく。

import gulp from 'gulp';

gulp.task( 'default', () => {
  console.log( 'test' );
} );

実行してみる。

$ npm start

> gulp-es2015@1.0.0 start .../sample
> gulp

[16:20:03] Requiring external module babel-register
(node:8531) fs: re-evaluating native module sources is not supported. If you are using the graceful-fs module, please update it to a more recent version.
[16:20:04] Using gulpfile .../sample/gulpfile.babel.js
[16:20:04] Starting 'default'...
test
[16:20:04] Finished 'default' after 208 μs

タスクは正常に実行された。

しかし gulp の参照している graceful-fs が古いためだろうか、常に警告が表示される。なんとかして欲しいものだ。

gulp-stylus の Source Maps 修正

gulp 版の browser-sync も npm-scripts と同様に server と startPath を分けて見たのだが、Stylus の Source Maps がうまく参照できなかった。調査したところ、gulp.src で base オプションを指定すれば適切に参照できることがわかった。

また front-end-starter と同じく normalize.css を npm で管理して組み込むように修正してみた。それら全てを反映した css タスクは以下となる。

gulp.task( 'css', () => {
  return gulp.src( [ config.dir.stylus + 'App.styl' ], { base: config.dir.root } )
    .pipe( $.plumber() )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.init() ) )
    .pipe( $.stylus( { 'include css': true } ) )
    .pipe( $.rename( 'bundle.css' ) )
    .pipe( $.if( config.isRelease, $.cleanCss() ) )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.write( './' ) ) )
    .pipe( $.if( config.isRelease, gulp.dest( config.dir.dist ), gulp.dest( config.dir.assets ) ) );
} );

import や config の定義については examples-web-app/gulpfile.babel.js を参照のこと。

修正を反映した後に npm start して browser-sync が適切にページを表示すること、その状態から Source Maps を参照できることを確認済み。