12
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-13

Promisebluebirdに置換するプラグインを作成してみました。その手順を整理したいと思います。

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

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

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

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

環境:

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

最低限、動作するプラグインについて

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

plugin.js
module.exports = (babel) => {
  console.log('plugin enabled')
  return {}
}
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で終了処理を行えます。

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')
        }
      }
    }
  }
}
node -e "console.log(require('babel-core').transform('code',{plugins:['./plugin.js']}).code)"
# plugin enabled
# program enter
# program exit
# code;

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

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')]
            )
          ])
        }
      }
    }
  }
}
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を使用して、実行結果を検証する1

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

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
# ...
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"
  }
}
.babelrc
{
  "presets": [
    "es2015"
  ],
  "env": {
    "development": {
      "sourceMap": "inline",
      "presets": [
        "power-assert"
      ]
    }
  }
}
src/index.js
module.exports = (babel) => {
  console.log('plugin enabled')
  return {
    visitor: {
    }
  }
}
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ではないため)。

構文の検出と変更

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

  • new Promise((resolve)=> resolve())
  • Promise.methods()
  • () => Promise.methods()
  • () => { var foo = Promise.methods(); return foo })
  • etc...

これら、想定した構文をastexplorerに通して、ASTを把握しながら、変更を加えます。

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

スクリーンショット 2016-05-13 16.30.08.gif

という風に、CallExpressionというASTが返ります。これをpluginのvisitor.CallExpressionから、Promise.resolveを検出し、Bluebird.resolveに変更します。

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で構文を限定する。
  • file.addImportで現在のファイルにimport文を追加、bluebird.resolveの参照をIdentifierで返す。
  • Promise.resolveの参照位置であるpath.node.callebluebird.resolveの参照で上書き。
"use strict";

var _bluebird = require("bluebird");

(0, _bluebird.resolve)();

結果、上記のようなコードを生成します(assert前にconsole.logresult.codeを確認しています)。

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

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

参考

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9