ES Modulesのimportで絶対パスを使いつつ、eslintでインポート先モジュールも検証させる

[Firefox 60からES Modulesにネイティブで対応するようになった](https://hacks.mozilla.org/2018/05/firefox-60-modules-and-more/)ことから、筆者は現在、（[人に言われて](https://github.com/piroor/treestyletab/issues/1935)）自作アドオンのES Modules化を進めています[^status]。

[^status]: [Mozilla Add-onsのファイルアップロード時の検証器がES Modulesに対応していない](https://github.com/mozilla/addons-linter/issues/1775)ためリリース版には反映できていないのですが、現在の所、[ツリー型タブ](https://addons.mozilla.org/firefox/addon/tree-style-tab/)と[マルチプルタブハンドラ](https://addons.mozilla.org/firefox/addon/multiple-tab-handler/)は作業を概ね終えています。

ES Modules化の最大のモチベーションは、*eslintでのより厳密な検証が可能になる*という点です。ES Modulesの仕様に則って書かれたモジュール群は、モジュール外で定義された変数や関数の事を考慮しなくてもよくなるため、変数名や関数名のtypoによって発生する「未定義の変数を参照している」「未定義の関数を呼ぼうとしている」といった状況まで静的に検出できるようになります[^lint]。

[^lint]: 具体的にどのようなルールを使えているかは、[2018年7月31日現在「ツリー型タブ」で使用している.esrintrc.jsの内容](https://github.com/piroor/treestyletab/blob/c26a48f1c01cc3b0da88330649760ab2eb9a854b/webextensions/.eslintrc.js)も併せてご覧下さい。

ただ、eslintで[eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import)を使ってインポート先のモジュールまで含めて検証するためには、`from`に書かれたパスを辿れる必要があります。なので普通にやろうとすると相対パスを使ってこんな風に書く事になります。

```javascript
import * as Constants from '../../common/constants.js';
import * as ApiTabsListener from '../../common/api-tabs-listener.js';
import * as MetricsData from '../../common/metrics-data.js';
import * as ApiTabs from '../../common/api-tabs.js';
import * as Tabs from '../../common/tabs.js';
import * as TabsContainer from '../../common/tabs-container.js';
import * as TabsUpdate from '../../common/tabs-update.js';
```

階層が深くなると`../`を何個も書かなければなりませんし、ファイルを置く階層を変える時もファイル間の位置関係を考慮してパスを直さなければなりません。これは地味に大変です。

ところで、Firefoxの拡張機能では構成ファイルのパスを示すにあたって、拡張機能のプロジェクトルートからの位置を`/`から始まる絶対パスとして記述できます。なので`import`文でも是非そうしたい所なのですが、そうするとeslintでの検証ができなくなります。`/`から始まる絶対パスで`import`させつつeslintでの検証も行う方法は無いものか……はい、あります。そこで登場するのが[eslint-import-resolver-babel-module](https://github.com/tleunen/eslint-import-resolver-babel-module)です。

eslint-import-resolver-babel-moduleを使うには、まず`package.json`に依存関係を追加し、それらを`npm install`でインストールします。

```diff:package.jsonの変更点
   },
   "dependencies": {
     "eslint": "^5.0.1",
-    "eslint-plugin-import": "^2.13.0"
+    "eslint-plugin-import": "^2.13.0",
+    "babel-core": "^6.0.0",
+    "babel-plugin-module-resolver": "^3.0.0",
+    "eslint-import-resolver-babel-module": "^4.0.0"
   }
 }
```

`eslint-import-resolver-babel-module`だけでなく`babel-core`と`babel-plugin-module-resolver`もわざわざ`dependencies`に追加しているのは、筆者環境では以下のように警告されたためです。

```
npm WARN eslint-import-resolver-babel-module@4.0.0 requires a peer of babel-core@^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN eslint-import-resolver-babel-module@4.0.0 requires a peer of babel-plugin-module-resolver@^3.0.0-beta but none is installed. You must install peer dependencies yourself.
```

次に、.eslintrc.jsの`module.exports`に必要な設定を追加します。

```diff:.eslintrc.jsの変更点
 module.exports = {
   'root': true,
   'parserOptions': {
     'ecmaVersion': 2018,
   },
   'env': {
     'browser': true,
     'es6': true,
     'webextensions': true,
   },
+  'settings': {
+    'import/resolver': {
+      'babel-module': {
+        'root': ['./'],
+      }
+    }
+  },
```

eslint-import-resolver-babel-moduleの使い方の情報を検索すると`'babel-module': {}`とだけ書いておく例が結構出てきた[^example]のですが、筆者環境では`'root': ['./']`という指定で明示的にプロジェクトルートを絶対パスのルートに紐付ける必要がありました。

[^example]: 実際、自分は[importを絶対パスで書く](https://qiita.com/maaz118/items/e09c14b94e2cedba1d9b)に書かれていた内容をヒントにして作業しました。

最後に、各モジュールの`import`文の`from`に書かれた相対パスを絶対パスに直しておきます。コマンド操作でやるなら以下の要領です。

```bash:importで相対パスを使っている部分を絶対パスに直すコマンド列の例
git grep "from '../" |
  cut -d : -f 1 |
  uniq |
  xargs sed -i -r -e "s;from '(../)+;from '/;g"
```

（これはUbuntu 16.04LTSでの例なので、GNU sedのオプション形式になっています。macOSの場合はBSD sedなので、`-r`ではなく`-E`と指定しなくてはなりません。）

これで、相対パスで`import`していた箇所が以下のような`/`から始まる絶対パス表記に置き換わります。

```javascript
import * as Constants from '/common/constants.js';
import * as ApiTabsListener from '/common/api-tabs-listener.js';
import * as MetricsData from '/common/metrics-data.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Tabs from '/common/tabs.js';
import * as TabsContainer from '/common/tabs-container.js';
import * as TabsUpdate from '/common/tabs-update.js';
```

以上の手順で、筆者環境では無事に絶対パスで`import`されたモジュールまで含めた検証が行われるようになりました。