この記事はECMAScript 2015の事始めとして、ライブラリをECMAScript 2015で書いて公開するというところから始めるのがいいのではという内容です。 ECMAScript 2015(ES2015)はES6とも呼ばれていてどちらも同じものを指しますが、この記事ではES2015に統一します。

ECMAScriptのバージョンについては次のページを参照してください。

2018-12-27: 追記

2018-10-06: 追記

2016-08-31: 追記

2015-05-09: 追記

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

ES2015 -> ES5

ES2015は策定が完了し、メジャーなブラウザの最新版は既に実装されています。

しかし、現実にはIE 11など最新版以外の実行環境でも動くように書くと思います。 また、ECMAScriptは毎年仕様がアップデートされるため、最新の仕様がすべてのブラウザで実装されているとは限りません。 そのため、新しい構文を含むコードをここではES5相当のコードに変換して利用します。

ES5の構文ならばIEを含むほぼすべてのブラウザで動作します。

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

少し注意したほうがいいのは、上記のツールは基本的には構文(classmoduleなど)の変換だけをサポートする点です。 構文とはサポートしてないコードをは読み込んだだけでシンタックスエラーとなるものを考えると分かりやすいです。

一方、Array.fromPromiseといったAPIは別のpolyfillと呼ばれるライブラリを使います。 polyfillとは仕様と同じ機能を実現するライブラリのことです。

そのため、変換ツールで変換したからといってすべてのES2015のAPIが古いブラウザで動作するわけではないことには注意が必要です。 たとえばTypeScriptはデフォルトでは構文のみを変換するため、TypeScriptで変換したからといってIE11でArray.fromが使えるわけではありません。 またES5へ変換やPolyfillを入れてもProxyなどES5の機能ではエミュレートできないものも存在することには注意が必要です

polyfillについては次のものが有名です。

簡単にまとめると、ES2015以降の構文を使うことでコードもシンプルになりますが、それを公開するためには変換ツールで変換して使うのが現状だと思います。

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

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

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

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

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

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

file tree]

追記(2018-12-27): textlint/textlint-rule-helperのmasterはTypeScriptの実装へ変換されています。 そのため、Babelを使った実装を参照したい方は 2.0.1 のバージョンのリポジトリを参照してください。

ローカルでBabelの実装をcheckoutしたい場合は次のようにタグをcheckoutしてください。

git clone git@github.com:textlint/textlint-rule-helper.git
cd textlint-rule-helper
git checkout 2.0.1

ライブラリの構成

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

  • src/: ES2015で書かれたコード
  • lib/: babelでsrc/以下のコードを変換したES5のコード
    • lib/ディレクトリは.gitignoreで無視して、リポジトリには含めない
    • 変換後のソースはGitで管理しない(そうした方がPull Request時に迷わない)
    • 逆にpackage.jsonfilesフィールドでは"files" : ["lib"]のみとする
    • npmが軽量になるし、npmで取ってきたファイルはlib/以下のES5のコードのみにできる
    • 細かすぎて伝わらない package.json 小ネタ三選 - t-wadaのブログ
  • test/: ES2015 + mochaでテストを書く
  • README.md: ドキュメント
    • ライブラリにはドキュメントは必要。いくら良いコードでもドキュメントがないとダメ
    • JSDocでドキュメントを書いてjsdoc-to-markdownでMarkdownに変換して、READMEに貼り付ける

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

tree

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

src/

ES2015で書いたコードを置く場所です。 src/はソースコードを置く場所として一般的に使われているディレクトリです。

lib/

実際に配布されるJavaScriptのコードを置く場所です。 Node.jsなど変換しないでコードを書く場合には、直接lib/以下にJavaScriptのソースをおいてると思います。

ここに置かれるJavaScriptファイルはsrc/のES2015で書かれたものをES5に変換したものが配置されます。 lib/src/から生成されるため、Gitで管理する必要性はありません。

BabelでES2015からES5の変換

ES2015 -> ES5の変換にはBabelを使います。

devDependenciesで入れておいて、npm run-scriptで実行するようにすればglobalに入れる必要はありません。 (Node.jsのプロジェクトにおいては、グローバルではなくローカルに依存モジュールを入れるのが一般的です。)

次のようにBabelとES2015からES5への変換するルールを含むpreset-envdevDependenciesへインストールします。

npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/register

そして、変換にどのpresetを使うかを.babelrcというファイル名で作成して次のような内容にします。 ここでは、@babel/preset-envだけを使うという指定になります。 @babel/preset-envではオプションで変換対象を細かく制御できますが、デフォルトではES5相当のコードへ変換してくれます。

.babelrc: { "presets": [ "@babel/preset-env" ] }

npm run-scriptsの追加

次のようにpackage.jsonscriptsフィールドにbuildwatchtestのスクリプトを追加します。

  ...
  "scripts": {
    "build": "babel src/ --out-dir lib/ --source-maps",
    "watch": "babel src/ --out-dir lib/ --watch --source-maps",
    "test": "mocha test/ --require @babel/register",
    "prepublish": "npm run build"
  }
  ...

このnpm run-scriptsは次のように、npm run <コマンド名>で実行できます。

例) babelを使ってsrc/のコードを変換し、lib/に出力する

npm run build

prepublishはライフサイクルコマンドで、npm publishを行う際に自動的に実行されます。 この場合はnpm publishする際に自動的にビルドを行うことで、ビルド忘れを防止できます。

.gitignoreで変換後のファイルを管理下から外す

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

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

/lib

このように設定すればGitHubなどには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/textlint/textlint-rule-helper/",
  "repository": {
    "type": "git",
    "url": "https://github.com/textlint/textlint-rule-helper.git"
  },
  "main": "lib/textlint-rule-helper.js",
  "files": [
    "lib/"
  ]
}

package.jsonREADME.mdLICENSEといった標準的なファイルは、デフォルトで含まれるようになってるので書かなくても問題ありません。

公開後にnpm info <パッケージ名>で何か含まれているかを知ることができます。

npm info textlint-rule-helper

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

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

npm publishでの公開方法については他の記事を参照してください。


test/

テストもES2015で書き、ここではテストフレームワークとしてMochaを使います。

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

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

しかし、このままだとimport文などES5ではないコードを実行することになるためテストコードもES5へ変換する必要があります。

テストにおいては変換方法として主に2つの方法があります

  • ソースコードとテストコードを事前に変換(npm run build)して、ES5へ変換済みのJavaScriptとしてテストコードを実行する
  • ソースコードとテストコードを実行時に変換しながらテストする

ソースコードとテストコードを事前に変換

これは、Babelでソースコードやテストコードを変換してから、そのES5のコードに対してmochaで実行するという流れが必要になります。

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

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

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

この手法はテストコードも変換する設定を追加する必要があるため、確実ですが少し手間がかかります。

ソースコードとテストコードを実行時に変換

Node.jsにはモジュールを読み込む際にBabelなどの変換をランタイムに行える仕組みがあります。 これを利用することで、最小限の設定でテストコードとソースコードを変換しながらテストを実行できます。

次のようにmocha--require引数に@babel/registerを設定します。 意味としては@babel/registerモジュールをロードして、JavaScriptファイルに対してはBabelの変換をランタイム時におこないます。

mocha test/ --require @babel/register

このようにするだけでES2015で書かれたテストコードを使ってテストできます。 テストの実行もnpm run-scriptとして実行できるようにscriptsに追加しておくと良いです。

次のように追加しておけば、npm run test または npm test(テストはショートカットが用意されている)でテストを実行できます。

  ...
  "scripts": {
    "build": "babel src/ --out-dir lib/ --source-maps",
    "watch": "babel src/ --out-dir lib/ --watch --source-maps",
    "test": "mocha test/ --require @babel/register",
    "prepublish": "npm run build"
  }
  ...

Tips

ES2015ではArrow Functionが使えるので、テストコードは以下のように書くことが出来ます。

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

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

Document

追記(2018-10-06): 現在はES2015に対応したドキュメントツールもあります。

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

75lb/jsdoc-to-markdownを使って出力したMarkdownをREADME.mdに貼り付けています。

エディタ

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

ES2015の書き方

ES2015以降(ES2016, 2017なども含む)をベースに学ぶJavaScriptについての書籍を書いています。 基本的な構文などは大体カバーしているので参考にしてみてください。

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

特にES2015 modulesはちょっと書き方が多いですが、仕様のシンタックスも固まったので慣れておきましょう。

ライブラリで今までクラスっぽいものを書いて公開してるケースは、class構文を使うことでシンプルに書けるようになるのでおすすめです。

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

ES2015に対応している書籍: