この記事はECMAScript 6の事始めとして、ライブラリをES6で書いて公開するというところから始めるのがいいのではという内容です。

2016-08-31: 追記

2015-05-09: 追記

  • 6to5 は Babel へリネームされました。
  • azu/espower-babelはテストコード以外もBabelで変換するようになりました
    • テストからimport hoge from "../lib/hoge"ではなくimport hoge from "../src/hoge"を参照できるようになりました

現在のES6 -> ES5

ECMAScript 6は現在策定の最終段階で、メジャーなブラウザの最新版は多くの機能を既に実装しています。

しかし、現実には最新版以外の実行環境でも動くように書くと思うので、ES6のコードをES5に変換して利用すると思います。

今すぐ、ES6でコードを書いてES5に変換するツールは色々あります。

少し注意したほうがいいのは、上記のツールは構文(classmoduleなど)をサポートという意味が多いです(そうじゃないとシンタックスエラーになるので)。

一方、Array.fromPromiseといったAPIは別のpolyfill(またはruntimeなどと言わわれるもの)を使いサポートしているという違いがある事には少し注意しておいたほうがいいです。

なので、こういった変換ツールを使う際は構文のサポートを求める感じで使うのがいいと思います。逆にpolyfillで対処できる機能は別にES6で書かなくても利用することができます。

polyfillについては以下を見ていくと面白いと思います。

簡単にまとめると、ES6の構文を使うことでコードもシンプルになりますが、それを公開するためには変換ツールを使わないといけないというのが現状だと思います(一種のAltJSみたいなもの)

ライブラリをES6で書いてnpmで公開してみよう

じゃあ手始めにどういう時にES6で書くのがいいかというと、ライブラリとして公開するようなコードをES6で書くのがいいと思います。

具体的な例と共にES6で書いてnpmでライブラリを公開するまでを見て行きたいと思います。

扱う題材をシンプルにするためにNode.jsのライブラリとして考えます。

以下のazu/textlint-rule-helperというものを例にして見ていきます。(このライブラリはtextlintというツールのルールを書くのを補助するライブラリですが今回はどうでもいいです)

上記のリポジトリをみてもらうと、ES5のコードやgulp等の設定ファイルも存在しない事がわかると思います。

file tree

ライブラリの構成

ライブラリの構成は以下のようになっています。

  • src/: ES6で書かれたコード
  • lib/: babelでsrc/以下のコードを変換したES5のコード
    • lib/ディレクトリは.gitignoreで無視して、リポジトリには含めない
    • 変換後のソースはGitで管理しない(そうした方がPull Request時に迷わない)
    • 逆にpackage.jsonfilesフィールドでは"files" : ["lib"]のみとする
    • npmが軽量になるし、npmで取ってきたファイルはlib/以下のES5のコードのみにできる
    • 細かすぎて伝わらない package.json 小ネタ三選 - t-wadaのブログ
  • test/: ES6 + mocha + power-assertでテストを書く
    • azu/espower-babelを使うことで、mocha --compilers js:espower-babel/guessと指定するだけでテストもES6で書ける
  • README.md: ドキュメント
    • ライブラリにはドキュメントは必要。いくら良いコードでもドキュメントがないならゴミ
    • JSDocでドキュメントを書き、jsdoc-to-markdownでREADMEに追加出来るように自動化する
    • jsdoc-to-markdownを使うことで、template/のテンプレートを使ってREADMEを生成できる
    • JSDocを変更する度にREADMEを手動で変更する必要がなくなる

ざっくり見るとazu/textlint-rule-helperは上記のような構成で動いています。

tree

(OmniGraffleのアイコンにディレクトリをD&Dするとこういう図ができる - OmniGraffle Folder Trick — MacSparky)

src/

ES6で書いたコードを置く場所です。AltJSでもsrc/にAltJSのソースを置くのと同じような話です。

lib/

JavaScriptのコードを置く場所です。Node.jsでは一般的にlib/以下にJSのソースをおいてると思います。

ここに置かれるJSはsrc/のものをES5に変換したものが配置されます。

ES6 -> ES5の変換にはBabelを使っています。devDependenciesで入れておいて、npm run-scriptで実行するようにすればglobalに入れる必要はありません。

次のようにBabelと変換ルールを決めたpresetをインストールします。 (加えてpower-assertも一緒に入れておきます)

npm i -D babel-cli babel-preset-es2015 babel-preset-power-assert babel-register power-assert

そして、変換にどのpresetを使うかを.babelrcというファイル名で作成して次のような内容にします。

{
  "presets": [
    "es2015"
  ],
  "env": {
    "development": {
      "presets": [
        "power-assert"
      ]
    }
  }
}
  ...
  "scripts": {
    "build": "NODE_ENV=production babel src --out-dir lib --source-maps inline",
    "watch": "babel src --out-dir lib --watch --source-maps inline",
    "test": "mocha --compilers js:babel-register test/*.js"
  }
  ...

しかし、変換したJSにPull Requestとか送られても困るのでGitリポジトリにはlib/は含めないようにします。

.gitignoreに以下のように書いて、libディレクトリを無視させます。

/lib

逆に、npm publishで公開するときには、ES5に変換されたコードだけを公開したいです。 そういう時は、package.json"files"フィールドに、ホワイトリストで含めたいファイル or ディレクトリを指定出来ます。

lib/だけを含めたいので以下のように書くことが出来ます。

{
  "name": "textlint-rule-helper",
  "description": "Helper for textlint rule.",
  "version": "1.1.2",
  "homepage": "https://github.com/azu/textlint-rule-helper/",
  "repository": {
    "type": "git",
    "url": "https://github.com/azu/textlint-rule-helper.git"
  },
  "main": "lib/textlint-rule-helper.js",
  "files": [
    "lib"
  ]
}

package.jsonやREADMEやLICENSE、CHANGELOG等のファイルはデフォルトで含まれるようになってるので書かなくても問題ありません。

npm info textlint-rule-helper

で直接.tgzファイルを落としてみると含まれてることが分かると思います。

ホワイトリストで指定する事でnpm packageのサイズが小さくなるというメリットもあるので、自然と取り入れやすいです。


test/

テストもES6で書き、power-assertを使いMochaでテストします。

テストファイルではES6で書かれたsrc/にあるコードをimportします。

import {RuleHelper} from "../src/textlint-rule-helper.js";
// package.json に "main": "lib/textlint-rule-helper.js", とある場合
import {RuleHelper} from "../"; // でも同じ意味になる

しかし、このテスト構成だと、BabelはES6->ES5の変換、power-assertはJS->JSの変換を行う必要があります。

つまり、babelで変換してからそれを更にpower-assert向けに変換し直して、mochaで実行するという流れが必要になります。

gulpで表現すると以下の様な流れですね。

gulp.task('powered-test', function () {
    return gulp.src(TEST)
        .pipe(sourcemaps.init())
        .pipe(to5())
        .pipe(espower())
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('./powered-test/'));
});

実際にgulpでやる場合は以下のリポジトリ等を参考にしてみるといい気がします。

自分は設定ファイルとかを別途作成するのは好きじゃないので、azu/espower-babelというモジュールを作成しました。

azu/intelli-espower-loaderと大体同じような事をやっていて、babel + power-assertの変換を一緒に実行時にやってくれるhookスクリプトです。

先ほど、presetとしてpower-assertの変換をやってくれる babel-preset-power-assert をインストールしたので、次のようにmocha--compilers引数を設定します。

意味としては、js拡張子には babel-register モジュールをロード(Babelの変換をランタイム時に行う)するという形になります。

mocha --compilers js:babel-register test/**/*.js

とするだけで、ES6で書かれたテストコードをpower-assertを使ってテストすることが出来ます。

詳しくは次の記事を参照してください。

power-assertを使わない場合は以下のように書くだけで問題ないので、azu/espower-babelは不要です。

mocha --compilers js:babel-register test/**/*.js

Tips

ES6ではArrow Functionが使えるので、以下のように書くことが出来ます。

describe("#getParents()", ()=> {
   context("on Document", ()=> {
       it("should return []", () => {
           var text = "# Header";
           var parents = [];
           assert(parents.length === 0);
       });
   });
});

しかし、thisの扱いが違うのでテストケース間でthis.prop的なやり取りをしたい場合は気をつけましょう。

Document

追記(2016-08-31): 現在はES2015+に対応したドキュメントツールもあるので、そちらを使ったほうがいいかもしれません。

このライブラリは小さいことを前提としてたので、README.mdにAPIの一覧をコード内のJSDocから生成して出しています。

75lb/jsdoc-to-markdownを使い、README.template.mdを用意してこのファイルからJSDocの情報を使ったREADME.mdを生成しています。

  "scripts": {
    "docs": "jsdoc2md -t template/README.template.md lib/textlint-rule-helper.js > README.md",
  },

正直この方法はかなり限定的で、色々環境で上手く働く感じではない気がしてるので、README駆動とかソースコードとドキュメントの分離とかもっと別の方法があるかもしれません。

理想的にはd.tsかWebIDLでインターフェースを書いてドキュメント化できるといい気がしますが今のところツールがないので。

エディタ

好きなエディタをつかって下さい。

WebStorm等のJetBrains IDEのES6対応は微妙に古い所があるので、casser/intellij-es67のプラグインを一緒に入れるとES6 modulesの扱いなどが改善します。

ES6

各自調べて下さい。

構文だけに限ってみてもTemplate Stings、Spread operator、Computed Property Names、ES6 classesなど、コードがシンプルに書きやすくなる部分が多いので、小さいライブラリを書くことで慣れておくといい気がします。

特にES6 modulesはちょっと表現力が高くて、仕様のシンタックスも固まったので慣れておきましょう。

ライブラリで今までクラスっぽいものを書いて公開してるケース等はES6 classesを使えば、シンプルに書けるようになるのでおすすめです。

export class RuleHelper {
  // コンストラクタ
  constructor(ruleContext) {
    this.ruleContext = ruleContext;
  }
  // 静的メソッド - RuleHelper.getHoge()
  static getHoge(){
  }
  // プロトタイプのメソッド - ruleHelper.isFuga()
  isFuga(){
  }
}

ES6の書籍

ES6の仕様についてのコミュニティ