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


Firefox 60からES Modulesにネイティブで対応するようになったことから、筆者は現在、(人に言われて)自作アドオンのES Modules化を進めています1

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

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

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です。

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

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-corebabel-plugin-module-resolverもわざわざdependenciesに追加しているのは、筆者環境では以下のように警告されたためです。

npm WARN [email protected] requires a peer of babel-core@^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN [email protected] 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に必要な設定を追加します。

.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': {}とだけ書いておく例が結構出てきた3のですが、筆者環境では'root': ['./']という指定で明示的にプロジェクトルートを絶対パスのルートに紐付ける必要がありました。

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

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していた箇所が以下のような/から始まる絶対パス表記に置き換わります。

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されたモジュールまで含めた検証が行われるようになりました。


  1. Mozilla Add-onsのファイルアップロード時の検証器がES Modulesに対応していないためリリース版には反映できていないのですが、現在の所、ツリー型タブマルチプルタブハンドラは作業を概ね終えています。 

  2. 具体的にどのようなルールを使えているかは、2018年7月31日現在「ツリー型タブ」で使用している.esrintrc.jsの内容も併せてご覧下さい。 

  3. 実際、自分はimportを絶対パスで書くに書かれていた内容をヒントにして作業しました。