babel@6プラグインの単純な作成方法について

[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)を[bluebird](https://github.com/petkaantonov/bluebird#readme)に置換するプラグインを[作成してみました](https://github.com/59naga/babel-plugin-transform-bluebird)。その手順を整理したいと思います。

記事中で取り上げること：

* 最低限、動作するプラグインについて
* `babel@6`プラグインの基本的な動作
* テスト環境の整備
* 構文の検出と変更

記事中で取り上げないこと：

* pluginのAPIや、`babel`引数の詳しい仕様
* `npm`, `package.json`, `mocha`, `TravisCI`など

環境：

```bash
nvm install v6.1.0 && node -v && npm -v
# v6.1.0
# 3.8.6
```

最低限、動作するプラグインについて
---
トランスパイルとプラグインの起動には`babel-core`が最低限必要になるので、これをインストールします。インストール後、REPLなどから、プラグインを起動させることが出来ます。

```js:plugin.js
module.exports = (babel) => {
  console.log('plugin enabled')
  return {}
}
```

```bash
node -e "console.log(require('babel-core').transform('code',{plugins:['./plugin.js']}).code)"
# plugin enabled
# code;
```

`code` -> `code;` というトランスパイル結果を得られました。

babel@6プラグインの基本的な動作
---

pluginの戻り値`visitor.Program.enter`でファイルの初期化処理、`visitor.Program.exit`で終了処理を行えます。


```js:plugin.js
module.exports = (babel) => {
  console.log('plugin enabled')
  return {
    visitor: {
      Program: {
        enter (path, file) {
          console.log('program enter')
        },
        exit (path, file) {
          console.log('program exit')
        }
      }
    }
  }
}
```

```bash
node -e "console.log(require('babel-core').transform('code',{plugins:['./plugin.js']}).code)"
# plugin enabled
# program enter
# program exit
# code;
```

`visitor`のそれぞれの第一引数`path`には対象のASTが渡されます。
`babel.template`や`babel.types`を利用してASTを生成し、プログラムの先頭や末尾にコードを追加することが可能です。

```js:plugin.js
module.exports = (babel) => {
  console.log('plugin enabled')
  return {
    visitor: {
      Program: {
        enter (path, file) {
          console.log('program enter')
          path.unshiftContainer('body', babel.template('foo; bar')())
        },
        exit (path, file) {
          console.log('program exit')

          path.pushContainer('body', [
            babel.template("console.log('hello world')")()
          ])

          // `babel.template("console.log('hello world')")()`とだいたい同じ
          const {
            callExpression,
            memberExpression,
            identifier,
            stringLiteral
          } = babel.types
          path.pushContainer('body', [
            callExpression(
              memberExpression(identifier('console'), identifier('log')),
              [stringLiteral('hello world')]
            )
          ])
        }
      }
    }
  }
}
```

```bash
node -e "console.log(require('babel-core').transform('code',{plugins:['./plugin.js']}).code)"
# plugin enabled
# program enter
# program exit

# foo;
# bar;
# code;
# console.log('hello world');
# console.log("hello world")
```

テスト環境の整備
---

`mocha`でテストコードからコンパイル結果を予測しながら、開発を行います。
テストの内容に関しては、プラグインの目的に応じて選択してください。

* トランスパイル後のコードが意図したコードに変換できたか確認するだけなら、`transform().code`を検証する
* トランスパイル後のコードの実行結果が、意図した状態になるか確認したいなら、[vm.runInNewContext](https://nodejs.org/dist/latest-v6.x/docs/api/vm.html#vm_vm_runincontext_code_contextifiedsandbox_options)を使用して、実行結果を検証する[^1]。

[^1]: 参考: [babel-plugin-transform-bluebird/test/index.js](https://github.com/59naga/babel-plugin-transform-bluebird/blob/master/test/index.js), [babel-plugin-add-module-exports/test/index.js](https://github.com/59naga/babel-plugin-add-module-exports/blob/master/test/index.js)

今回は後者を採用します。
新規にディレクトリを作成し、以下４ファイルを用意します。

```bash
mkdir babel-plugin-example
cd babel-plugin-example

tree .
# .
# ├── package.json
# ├── .babelrc
# ├── src
# │   └── index.js
# └── test
#     └── index.js


npm install
npm test
# ...
# AssertionError:   # test/index.js:21
# ...
```

```json:package.json
{
  "name": "babel-plugin-example",
  "private": true,
  "scripts": {
    "start": "mocha --require babel-register --watch",
    "test": "mocha --require babel-register"
  },
  "devDependencies": {
    "babel-core": "^6.8.0",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-power-assert": "^1.0.0",
    "babel-register": "^6.8.0",
    "bluebird": "^3.3.5",
    "mocha": "^2.4.5",
    "power-assert": "^1.4.1"
  }
}
```

```json:.babelrc
{
  "presets": [
    "es2015"
  ],
  "env": {
    "development": {
      "sourceMap": "inline",
      "presets": [
        "power-assert"
      ]
    }
  }
}
```

```js:src/index.js
module.exports = (babel) => {
  console.log('plugin enabled')
  return {
    visitor: {
    }
  }
}
```

```js:test/index.js
// dependencies
import assert from 'assert'
import { transform } from 'babel-core'
import vm from 'vm'
import Bluebird from 'bluebird'

// target
import plugin from '../src'

// specs
describe('babel-plugin-example', () => {
  it('should be injected if called built-in `Promise.resolve`', () => {
    const result = transform('Promise.resolve()', {
      presets: ['es2015'],
      plugins: [plugin]
    })
    const returnValue = vm.runInNewContext(result.code, {
      require: (name) => require(name)
    })

    assert(returnValue instanceof Bluebird)
  })
})
```

`npm test`で、テストが失敗することを確認します。
（トランスパイルしたコードの実行結果、そのインスタンスが`bluebird`ではないため）。

構文の検出と変更
---

実際に`Promise`を`bluebird`に書き換えるにあたって、対象となる構文が何ケース存在するか想定します。

* `new Promise((resolve)=> resolve())`
* `Promise.methods()`
* `() => Promise.methods()`
* `() => { var foo = Promise.methods(); return foo })`
* [etc...](https://github.com/59naga/babel-plugin-transform-bluebird/blob/master/test/specs.js)

これら、想定した構文を[astexplorer](https://astexplorer.net/)に通して、ASTを把握しながら、変更を加えます。

例えば、一番単純な例として`Promise.resolve()`を`Bluebird.resolve()`に置換する場合

![スクリーンショット 2016-05-13 16.30.08.gif](https://qiita-image-store.s3.amazonaws.com/0/28576/559c43c7-c33f-7b61-bfba-2713ee13657b.gif)

[という風](https://astexplorer.net/#/K61ECUFYfB)に、`CallExpression`というASTが返ります。これをpluginの`visitor.CallExpression`から、`Promise.resolve`を検出し、`Bluebird.resolve`に変更します。

```js:src/index.js
module.exports = (babel) => {
  return {
    visitor: {
      CallExpression (path, file) {
        if (path.get('callee').matchesPattern('Promise.resolve')) {
          path.node.callee = file.addImport('bluebird', 'resolve')
        }
      }
    }
  }
}
```

* [matchesPattern](https://github.com/babel/babel/search?utf8=%E2%9C%93&q=matchesPattern)で構文を限定する。
* `file.addImport`で現在のファイルに`import`文を追加、`bluebird.resolve`の参照を`Identifier`で返す。
* `Promise.resolve`の参照位置である`path.node.calle`を`bluebird.resolve`の参照で上書き。

```js
"use strict";

var _bluebird = require("bluebird");

(0, _bluebird.resolve)();
```

結果、上記のようなコードを生成します（`assert`前に`console.log`で`result.code`を確認しています）。

```bash
npm test
#  babel-plugin-example
#    ✓ should be injected if called built-in `Promise.resolve`
#
#  1 passing (7ms)
```

想定した処理を追加したため、テストも通るようになりました。

参考
---
* [AST explorer](https://astexplorer.net/)
* [babel/packages - github](https://github.com/babel/babel/tree/master/packages)
* [Babel Plugin Handbook（和訳途中）](https://github.com/thejameskyle/babel-handbook/blob/master/translations/ja/plugin-handbook.md)
* [Babel User Handbook（和訳途中）](https://github.com/thejameskyle/babel-handbook/blob/master/translations/ja/user-handbook.md)
* [babel6にmodule.exportsを追加するだけのプラグインを作った - qiita](http://qiita.com/59naga/items/10ec2b5ef0fd62a5b437)