JavaScriptのブラウザ互換性をESLintでチェックする

Webサイト制作でトランスパイル環境のない生のJavaScriptを書く機会があり、そこそこ古いバージョンのブラウザでも動くようにLintでコードチェックできたらなーと思って調べてみました。導入手順とかんたんな使い方を紹介します。

# 環境

- Node.js v20
- VS Code v1.82

# TL;DR

国内シェア1％以上、かつES6サポートのブラウザをターゲットとした例：

```json:package.json
{
  "scripts": {
    "lint": "eslint .; exit 0"
  },
  "devDependencies": {
    "eslint": "^8.51.0",
    "eslint-plugin-compat": "^4.2.0"
  },
  "browserslist": [
    ">= 1% in JP and not dead"
  ]
}
```

```js:.eslintrc.js
module.exports = {
  env: {
    browser: true,
  },
  extends: ["eslint:recommended", "plugin:compat/recommended"],
  overrides: [
    {
      env: {
        node: true,
      },
      files: [".eslintrc.{js,cjs}"],
      parserOptions: {
        sourceType: "script",
      },
    },
  ],
  parserOptions: {
    ecmaVersion: 6,
    sourceType: "module",
  },
  rules: {},
  plugins: ["compat"],
  settings: {
    polyfills: [],
  },
};
```

# セットアップ

package.jsonを作成します。

```shell
npm init -y
```

ESLintをセットアップします。

```shell
npm init @eslint/config

✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ What format do you want your config file to be in? · JavaScript
Local ESLint installation not found.
The config that you've selected requires the following dependencies:

eslint@latest
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · npm
```


ESLintのプラグインをインストールします。

```shell
npm install -D eslint-plugin-compat
```

- 参考）ESLintのセットアップ https://eslint.org/docs/latest/use/getting-started
- 参考）プラグインのインストール https://github.com/amilajack/eslint-plugin-compat

CLIでかんたんにLintが呼び出せるようにスクリプトを追加しておきましょう。

```json:package.json
"scripts": {
  "lint": "eslint .; exit 0"
}
```

VS Codeエクステンションの追加もオススメします。コードを書くとリアルタイムでチェックが実行されます。

https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint

# ターゲットを決める

すでに公開済みのサイトであれば、Googleアナリティクスなどを参考にアクセスの多いブラウザやバージョンの目処をつけておきます。

これから公開するサイトであれば、主要なブラウザの最新5バージョンなど制作者サイドでルールを決めるのも良いでしょう。

以降、サイトの訪問を意図したブラウザおよびバージョンを「ターゲット」と記載します。

# APIのチェック

Browserslistのサイトで、ターゲットに該当するブラウザとバージョンを確認してみましょう。

https://browsersl.ist/

最新5バージョンをターゲットとした例：

```
last 5 versions and not dead
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/c2e2fb7d-1621-f586-c01e-a494ffc696a6.png" width="500" alt="">

特定のブラウザをターゲットとした例：

```
Chrome >= 116 or FireFox >= 115
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/340b42a5-5b23-d992-c4cb-a93bb5876473.png" width="500" alt="">

`not dead` は過去1年間にアップデートされたブラウザのみ対象とします。これを指定しておくと、サポート終了してもなお使われ続けているブラウザを除外できます。

コードベース上のターゲット指定はpackage.jsonで行います。
ここではシェア1％以上をターゲットとしました。

```json:package.json
"browserslist": [
  ">= 1% and not dead"
]
```

ESLintプラグイン「eslint-plugin-compat」を有効化します。
このプラグインはターゲット指定を読み込んでサポートしていないAPIをエラーとして検出します。

```js:.eslintrc.js
extends: [
  "eslint:recommended",
  "plugin:compat/recommended"
],
plugins: ["compat"],
```

任意のファイルを作成して、試しにfetchAPIを使ってみましょう。
VSCodeエクステンションによりリアルタイムでエラーが検出されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/a3aeca20-9e0e-3edc-9798-146237aac199.png" width="700" alt="">

CLIでは以下のようにエラーが検出されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/4db61e47-4da7-7c6b-9177-b425a9fc3206.png" width="700" alt="">

記事執筆時点のシェア1％にはOperaモバイル版の「Opera Mini」が含まれ、これがfetchAPIをサポートしていません。 `in JP` を追加して国内シェアに絞り込んでみましょう。

```json:package.json
"browserslist": [
  ">= 1% in JP and not dead"
]
```

Opera Miniは国内シェアが低いためターゲットから外れ、エラーは検出されなくなりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/e15d219e-7b70-e042-50c9-40f6352f1bd1.png" width="700" alt="">

サイトのターゲットとするべきブラウザやバージョン、それらがサポートするAPI群を記憶しておくことは、人間にとってとんでもなく困難です。Lintでチェックできるなんて最高に便利ですね！

:::note warn
ここでひとつ注意事項があります。
「eslint-plugin-compat」はグローバルに存在するAPI（URLSearchParamsオブジェクトや、fetch関数など）をチェックしてくれますが **言語構文（letやスプレッド構文など）についてはチェックしない** ようです。理由についてissueの中で「トランスパイルできるものは対応の優先度が低いから」と述べられています。
https://github.com/amilajack/eslint-plugin-compat/issues/454
:::

試しにオプショナルチェーンを使ってみても、エラーは何も検出されません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/c71e7167-4ce4-933f-0798-2c461318fb70.png" width="700" alt="">

# 言語構文のチェック

言語構文についてはESLintの設定の中でECMAScriptレベルの指定ができます。browserslistの指定とはリンクしませんが、これだけでも大きな助けとなります。

デフォルトの設定は比較的新しいものになっているので、ES6レベルに下げてみましょう。

```js:.eslintrc.js
  env: {
    browser: true,
  },
  ...
  parserOptions: {
    ecmaVersion: 6,
    ...
  },
```

- 参考）ESLint https://eslint.org/docs/latest/use/configure/language-options#using-configuration-files
- 参考）サバイバルTypeScript https://typescriptbook.jp/tutorials/eslint#parseroptions

オプショナルチェーンはES2020で導入された構文です。ES6では解釈できずパースエラーとして検出されるようになりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/0c9e5e7d-7978-3622-c4a7-30e24c61a360.png" width="700" alt="">

CLIでも同じエラーが検出されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102284/b776bab2-8033-bc88-f61f-f796879543e8.png" width="700" alt="">

# おまけ

eslint-plugin-compatに付随してインストールされる「browserslist」コマンドを叩くと、ターゲットのブラウザとバージョンがリスト表示されます。User Agent Client Hintsと組み合わせてサイト訪問者にブラウザアップデートの補足を表示するなど活用できそうですね。

```shell
./node_modules/.bin/browserslist

> and_chr 117
> chrome 117
> chrome 116
> edge 117
> edge 116
> firefox 117
> ios_saf 17.0
> ios_saf 16.6
> safari 16.6
```

# おわりに

思い出せばAPIをひとつずつ [Can I use](https://caniuse.com/) で調べながらコードを書いていた時代もありましたが、機械的なチェックが格段に進化を遂げていて衝撃的でした。ESLintやプラグイン開発者の方々に感謝です！