babelにPRを書いてマージされた時の話

先々月、一月の話ですが、自分がリリースしたbabelプラグインの[issueの原因を調べていたところ](https://github.com/59naga/babel-plugin-add-module-exports/issues/20)、原因は[babel側のプラグイン内の処理がまずいこと](https://github.com/babel/babel/pull/3282/files#diff-89de1a20a1d7e0483c607274bcf877c4R64)でした。[^1]

```js:actual.js
export {default as foo} from "commonjs-module";
```

```js:expected.js
var _foo = require("commonjs-module");

Object.defineProperty(exports, "foo", {
  enumerable: true,
  get: function () {
    return _foo.default; // undefined!
  }
});
```

上記の修正を、babelの公式リポジトリへPRした時のことを、手順を思い出しながら書こうと思います。

>
* 修正した当時と現在で、レポジトリの開発環境がやや変わっているので、この記事は`v6.6.5`現在の環境で、PRを書く手順について説明します。

環境設定
---

まずbabel公式のリポジトリをフォークして、フォークした自分のレポジトリを`git clone`します。

![babel_babel__Babel_is_a_compiler_for_writing_next_generation_JavaScript_.png](https://qiita-image-store.s3.amazonaws.com/0/28576/d26f4e93-70b6-9c42-682b-07f6fcaad6be.png)

```bash
git clone https://github.com/59naga/babel
cd babel
```

[CONTRIBUTING.md](https://github.com/babel/babel/blob/master/CONTRIBUTING.md) に環境設定方法が載っているで、こちらを参考にテスト環境を整えます。
以下のコマンドで、依存モジュールのインストールと初期コンパイルを一括で行います。

```bash
make bootstrap
```

あと、`CONTRIBUTING.md` の冒頭にもある通り、`make bootstrap`の間にでも [Code of Conduct（和訳）](http://contributor-covenant.org/version/1/3/0/ja/) に目を通しておいて下さい。

>
* `v6.6.5` 現在、追加でモジュールをインストールする必要があります。[^2]
`npm install estraverse-fb`

https://github.com/babel/babel/tree/master/packages
に、大まかなプラグインの一覧が載っています。

目的のコンパイル結果を担当しているプラグインを、その構文の英名[^3]から推測するか、プラグイン内部にある`/test/fixtures/spec/`フォルダ内に、[コンパイル前とコンパイル後のコードが定義されている](https://github.com/babel/babel/tree/v6.4.5/packages/babel-plugin-transform-es2015-modules-commonjs/test/fixtures/interop/exports-from)ので、こちらから見つけます。
あるいは、githubの上部検索メニュー`This repository search`に直接コード片を入力してしまうのもありかもしれません。

今回のケースでは、`babel-plugin-transform-es2015-modules-commonjs`というプラグインで、以下のようなコードが生成されることを知りました。（下記は`v6.4.4`以前の結果です）

```js:actual.js
export {default as foo} from "commonjs-module";
```

```js:expected.js
var _foo = require("commonjs-module");

Object.defineProperty(exports, "foo", {
  enumerable: true,
  get: function () {
    return _foo.default;
  }
});
```

テストが通ることを確認します。[^4]

```bash
TEST_GREP=es2015-modules-commonjs make build test
# rm -rf packages/*/test/tmp
# ...
# [02:27:37] Finished 'build' after 301 ms
# ./node_modules/.bin/kcheck
# All good! ✨
# ./scripts/test.sh
# 
#   
#   ․․․․․․․․․․․․․․․․․․․․․․․․․․
# 
#   26 passing (1s)
# 
# make test-clean
# rm -rf packages/*/test/tmp
# rm -rf packages/*/test-fixtures.json
```

テストは通りましたが、このコンパイル結果では`export {default as foo} from "common-js-module";`が動作しません[^1]。

テストの追加、プラグインの修正
---

```js
export {default as foo} from "common-js-module";
```
上記コードを動作させるためには、下記のようなコンパイル結果になるのが望ましいです。

```js:expected.js
var _foo = require("commonjs-module");

Object.defineProperty(exports, "foo", {
  enumerable: true,
  get: function () {
    return babelHelpers.interopRequireDefault(_foo).default;
  }
});
```

>
`babelHelpers.interopRequireDefault(foo)`について

>
このコードは[babel-plugin-external-helpers](https://github.com/babel/babel/blob/master/v6.6.5/babel-core/src/tools/build-external-helpers.js#L85)を通して、最終的に下記のようなコードになります。

>
```js:expected.js
var foo = require("commonjs-module");
Object.defineProperty(exports, "foo", {
  enumerable: true,
  get: function get() {
    return _interopRequireDefault(foo).default;
  }
});
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
```

>
`babel`でコンパイルされていない素のjsライブラリなどには`exports.__esModule`が定義されていませんので、`_interopRequireDefault(foo)`は`{default: foo}`を返します。
その結果、`var expected = require('./expected.js')`したさい、`expected.foo`から`commonjs-module`の参照が返るようになります。

前述した`expected.js`を保存して、テストが失敗することを確認します。

```bash
TEST_GREP=es2015-modules-commonjs make build test-only
# ...
# ․․․․․․․․․․․․․․․․․․․․․․․․․․
# 25 passing (1s)
# 1 failing
#
# 1) babel-plugin-transform-es2015-modules-commonjs/interop exports from:
#     babel/packages/babel-plugin-transform-es2015-modules-commonjs/test/fixtures/interop/exports-from/actual.js !== /Users/59naga/Downloads/babel/packages/babel-plugin-transform-es2015-modules-commonjs/test/fixtures/interop/exports-from/expected.js
#     + expected - actual
#
#      });
#      Object.defineProperty(exports, "foo", {
#        enumerable: true,
#        get: function () {
#     +    return babelHelpers.interopRequireDefault(_foo).default;
#     -    return _foo.default;
#        }
#      });
```

テストを成功させるために、`Object.defineProperty(exports, ...`が、このpluginの`src/index.js`のどの辺りで定義されているか目星を付けますが、[行頭に`babel-template`が幾つか定義されています](https://github.com/babel/babel/blob/v6.4.4/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js#L5-L43)ので、そこからかなり目的の処理を絞れます。

>
* [よもやま話（babel-templateについて） - babel6にmodule.exportsを追加するだけのプラグインを作った](http://qiita.com/59naga/items/10ec2b5ef0fd62a5b437#%E3%82%88%E3%82%82%E3%82%84%E3%81%BE%E8%A9%B1)

```js:src/index.js#L15
let buildExportsFrom = template(`
  Object.defineProperty(exports, $0, {
    enumerable: true,
    get: function () {
      return $1;
    }
  });
`);
```

`buildExportsFrom`を[使用している行](https://github.com/babel/babel/blob/v6.4.4/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js#L273)が、問題の処理のようです。

```js:babel/blob/v6.4.4/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js#L265-L276
let ref = addRequire(source.value, path.node._blockHoist);

for (let specifier of specifiers) {
  if (specifier.isExportNamespaceSpecifier()) {
    // todo
  } else if (specifier.isExportDefaultSpecifier()) {
    // todo
  } else if (specifier.isExportSpecifier()) {
    topNodes.push(buildExportsFrom(t.stringLiteral(specifier.node.exported.name), t.memberExpression(ref, specifier.node.local)));
    nonHoistedExportNames[specifier.node.exported.name] = true;
  }
}
```

`{default as foo}`のような構文の時だけ、コンパイル結果を変更します。
>
* これがどのようなifを書けば良いのか分からなかったので、`if(specifier.isExportSpecifier()){...}`の中で`console.log(specifier)`でASTの中身を覗きました。
`specifier.node.local.name`に、リネーム前の参照名が定義されていました。

```js:babel/blob/v6.4.5/packages/babel-plugin-transform-es2015-modules-commonjs/src/index.js#L290-L304
let ref = addRequire(source.value, path.node._blockHoist);

for (let specifier of specifiers) {
  if (specifier.isExportNamespaceSpecifier()) {
    // todo
  } else if (specifier.isExportDefaultSpecifier()) {
    // todo
  } else if (specifier.isExportSpecifier()) {
    if (specifier.node.local.name === "default") {
      topNodes.push(buildExportsFrom(t.stringLiteral(specifier.node.exported.name), t.memberExpression(t.callExpression(this.addHelper("interopRequireDefault"), [ref]), specifier.node.local)));
    } else {
      topNodes.push(buildExportsFrom(t.stringLiteral(specifier.node.exported.name), t.memberExpression(ref, specifier.node.local)));
    }
    nonHoistedExportNames[specifier.node.exported.name] = true;
  }
}
```

`specifier.node.local.name`が`default`の時だけ、`buildExportsFrom`の第二引数`t.memberExpression`の第一引数`ref`を、`t.callExpression(this.addHelper("interopRequireDefault"), [ref])`に変更します。

>
* `t`は`babel-types`で、`buildExportsFrom`のような`babel-template`の引数に使用する[Flow](http://flowtype.org/)の型が定義されています。
https://github.com/babel/babel/blob/v6.4.5/lib/types.js#L859
* [this.addHelper](https://github.com/babel/babel/blob/v6.4.5/packages/babel-core/src/transformation/file/index.js#L262-L299)は第一引数を[babel-helpers](https://github.com/babel/babel/blob/v6.4.5/packages/babel-helpers/src/helpers.js#L236-L240)から取得し、`babel-template`を返します。（その際、helperが現在のAST内で未定義なら追加します）

[src/index.jsの変更点](https://github.com/babel/babel/pull/3282/files#diff-d14797ce7c2385d5701ea1ef6b4da702L298)は以上です。

テストが成功することを確認します。（また、`kcheck(eslint)`が通ることも確認します）

```bash
TEST_GREP=es2015-modules-commonjs make build test
# ./node_modules/.bin/kcheck
# All good! ✨
# ․․․․․․․․․․․․․․․․․․․․․․․․․․
# 
# 26 passing (1s)
```

コミットメッセージに修正内容を簡潔に記述し、pushします。

```bash
git commit -am "Fix T6953,T2541 export-from statement renamed default issue

- Use `interopRequireDefault` helper if local name is default.

Via 59naga/babel-plugin-add-module-exports#20"
```

>
* T***はbabelのissue管理に使われている https://phabricator.babeljs.io/ のissue番号です。
* `username/repository#issue-number` のような書式をコミットメッセージに書くと、github上では自動でgithub-issueにリンクが貼られるようです。（リンク先にもログが残りました）

フォークした自分のページの`New pull request`ボタンからPRを作成します。

![59naga_babel__Babel_is_a_compiler_for_writing_next_generation_JavaScript_.png](https://qiita-image-store.s3.amazonaws.com/0/28576/15b40e9a-dad1-f595-0c3d-ceb09f3d8722.png)

件名と本文を入力して`Send pull request`を押せば、あとは相手の返信を待つだけです。

![スクリーンショット 2016-03-08 04.05.24.gif](https://qiita-image-store.s3.amazonaws.com/0/28576/5d717493-e626-69f2-bb74-09ca90f1ae2f.gif)

>
* 初め、[複数のcommitでPRを行いましたが「squashしてね」と頼まれました](https://github.com/babel/babel/pull/3270#issuecomment-172711448)。
* [squashしたものの](http://changesworlds.com/2015/03/git-rebase-squash/)、pushしてもrejectされるので次の手順がよく分からず、レポジトリ破壊（何のコマンド入れたかよく覚えていない）することになったんですが、要約すると
>
```bash
git rebase -i HEAD~3
# pick yyyyyyy Modify 1
# squash zzzzzzz Modify 2
# squash 1111111 Modify 3
git push -f
```
という風に`git push -f`で歴史を改編する必要があったことを、後になって知りました。

おわりに
---
手探りのため、かなり理解が怪しいですが、PRを書く参考になれば幸いです。
誤字脱字、きわどい表現などの指摘を、お待ちしております。

[^1]: fix前の状態で`export {default as foo} from "common-js-module";`のような構文を使用すると、`foo`が存在しないプロパティ`.default`を参照して`undefined`を返す不具合でした。[CHANGELOG.md#v6.4.5(2016-01-19)](https://github.com/babel/babel/blob/master/CHANGELOG.md#645-2016-01-19)
[^2]: `./node_modules/.bin/kcheck`がコケますが、内部の[babel-eslintの依存モジュール定義不足が問題です](https://github.com/babel/babel-eslint/issues/267)。下記のコマンドで不足したモジュールを補います。
[^3]: `export from` やら `class` とか `function bind` など…
[^4]: [Running tests - CONTRIBUTING.md](https://github.com/babel/babel/blob/master/CONTRIBUTING.md#running-tests), `lint`がやけに重いので `make build test-only`でテストだけ走らせました