Promiseをbluebirdに置換するプラグインを作成してみました。その手順を整理したいと思います。
記事中で取り上げること:
- 最低限、動作するプラグインについて
-
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などから、プラグインを起動させることが出来ます。
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
で終了処理を行えます。
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.template
やbabel.types
を利用してASTを生成し、プログラムの先頭や末尾にコードを追加することが可能です。
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
# ...
{
"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"
}
}
{
"presets": [
"es2015"
],
"env": {
"development": {
"sourceMap": "inline",
"presets": [
"power-assert"
]
}
}
}
module.exports = (babel) => {
console.log('plugin enabled')
return {
visitor: {
}
}
}
// 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...
これら、想定した構文をastexplorerに通して、ASTを把握しながら、変更を加えます。
例えば、一番単純な例としてPromise.resolve()
をBluebird.resolve()
に置換する場合
という風に、CallExpression
というASTが返ります。これをpluginのvisitor.CallExpression
から、Promise.resolve
を検出し、Bluebird.resolve
に変更します。
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.calle
をbluebird.resolve
の参照で上書き。
"use strict";
var _bluebird = require("bluebird");
(0, _bluebird.resolve)();
結果、上記のようなコードを生成します(assert
前にconsole.log
でresult.code
を確認しています)。
npm test
# babel-plugin-example
# ✓ should be injected if called built-in `Promise.resolve`
#
# 1 passing (7ms)
想定した処理を追加したため、テストも通るようになりました。
参考
- AST explorer
- babel/packages - github
- Babel Plugin Handbook(和訳途中)
- Babel User Handbook(和訳途中)
- babel6にmodule.exportsを追加するだけのプラグインを作った - qiita