この記事は BASE Advent Calendar 2021 の 5 日目の記事です。
基盤チームの右京です。 最近ひょんなことから browserslist の設定を見返したのですが「babel や autoprefixer で必要になったので導入した」以上はあまり触れられていなかったため、この機会にいちから見直してみようと思いました。
browserslist?
簡単に言えば、クエリを書くとそれに該当するブラウザをリストで取得できます。babel(preset-env) や autoprefixer はここから取得出来るリストを利用して、必要な変換内容を決定しています。単純にバージョン指定でのクエリが記述できるだけではなく、利用統計に基づく絞り込みも可能となっています。例えば、0.2% 以上のシェアがあり、メンテナンスが行われているブラウザは次のクエリで取得できます。
> 0.2%, not dead
このクエリは package.json
の browserslist
として指定するか、 .browserslistrc
を作成して記述すると認識されるようになります。今回は package.json
に書く形で進めます。
{ "private": true, "browserslist": [ "> 0.2%, not dead" ] }
この状態になればブラウザのリストを CLI か JavaScript で取得できます。試しに CLI を使って取得してみます。
$ npx browserslist and_chr 96 and_ff 94 and_uc 12.12 chrome 95 chrome 94 chrome 93 chrome 92 chrome 87 chrome 61 chrome 49 edge 95 edge 94 firefox 93 firefox 92 ie 11 ios_saf 15 ios_saf 14.5-14.8 ios_saf 14.0-14.4 ios_saf 13.4-13.7 ios_saf 12.2-12.5 ios_saf 9.0-9.2 opera 79 safari 15 safari 14.1 safari 14 safari 13.1 samsung 15.0
babel からの利用
取得できたリストを babel(preset-env) がどのように利用しているかを確認してみます。利用には @babel/preset-env
を設定する必要があるので、これを追加して設定します。
{ "presets": ["@babel/preset-env"] }
次のようなコードを例に考えてみます。このコードは古いブラウザでは async/await が実装されていないため、このままでは動作できず何かしらの形に変換される必要があります。
(async () => { const response = await fetch('https://api.github.com/users/yaakaito'); console.log(response) })()
先程の browserslist の結果には async/await を実装していない IE11 が含まれているため、babel で変換すると置き換えがおこるはずです。
> npx babel src/index.js "use strict"; function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { var response; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return fetch('https://api.github.com/users/yaakaito'); case 2: response = _context.sent; console.log(response); case 4: case "end": return _context.stop(); } } }, _callee); }))();
このように IE11 でも動作可能なコードに変換されました。ですが、BASE では IE11 のサポートを終了したため、対象ブラウザから IE11 を除外したいと思います。これもクエリを書くことで表現することが出来ます。not IE 11
を追加します。
{ "private": true, "browserslist": [ "> 0.2%, not dead", "not IE 11" ] }
この状態で babel で改めて変換すると、対象にしているブラウザすべてで async/await が実装されているため変換が不要となり、出力されるコードが変化していることがわかります。
$ npx babel src/index.js "use strict"; (async () => { const response = await fetch('https://api.github.com/users/yaakaito'); console.log(response); })();
BASE の設定を見直す
さて、きっかけとなった今現在 BASE で設定されている browserslist を見ていきます。browserslist は複数のリポジトリで共有しているため、プライベートな GitHub Packages として配信しており、このような形で利用できるようにしています。
"browserslist": [ "extends @baseinc/browserslist-config" ]
そしてその中身は...ちょっと頭が痛くなる感じでした。
['ie >= 11', 'safari >= 7', 'iOS >= 10.0', 'and_chr >= 5.0', 'last 1 version', '> 1%']
and_chr 91 and_ff 89 and_qq 10.4 and_uc 12.12 android 91 baidu 7.12 bb 10 chrome 91 chrome 90 chrome 89 edge 91 edge 90 firefox 89 firefox 88 ie 11 ie_mob 11 ios_saf 14.5-14.6 ios_saf 14.0-14.4 ios_saf 13.4-13.7 ios_saf 13.3 ios_saf 13.2 ios_saf 13.0-13.1 ios_saf 12.2-12.4 ios_saf 12.0-12.1 ios_saf 11.3-11.4 ios_saf 11.0-11.2 ios_saf 10.3 ios_saf 10.0-10.2 kaios 2.5 op_mini all op_mob 62 opera 76 safari 14.1 safari 14 safari 13.1 safari 13 safari 12.1 safari 12 safari 11.1 safari 11 safari 10.1 safari 10 safari 9.1 safari 9 safari 8 safari 7.1 safari 7 samsung 14.0
明らかに内容が古いので、現在推奨している環境に合わせてクエリを見直します。
これをもとに考えると、このようなクエリが適用できそうです。
"browserslist": [ "> 0.2%, not dead", "not IE 11", "Safari > 11, iOS > 11" ]
他のツールにも活用したい
最近、TypeScript の変換を速度面の課題から tsc → esbuild に置き換えたいと考えていて、実は今回見直すきっかけになったのもこれでした。Vue.js 2x での TSX サポートのために Babel を通す必要があるのですが、それ以外のものに関してはできれば esbuild のみで解決したい。esbuild の target はブラウザの指定が可能ですが、browserslist には対応していません。
これに browserslist を使えるようにしてみます。すべて対応しようとすると大変なので、ここでは chrome
のみで考えます。
#!/usr/bin/env node const browserslist = require('browserslist'); const { buildSync } = require('esbuild'); const browsers = browserslist(); const target = [browsers.reverse().find(t => t.startsWith('chrome')).replace(' ', '')]; console.log(buildSync({ entryPoints: ['src/index.js'], target }))
簡易的ですが、browserslist に含まれている chrome
の一番古いバージョンを esbuild で解釈できる形に変換しています。これをビルドしてみると、先程と同じ結果になります。
> ./build.js { errors: [], warnings: [] } (async () => { const response = await fetch("https://api.github.com/users/yaakaito"); console.log(response); })();
試しに async/await が実装されていないバージョン(chrome54)が含まれるクエリに変更してみます。
"browserslist": [ "> 0.2%, not dead", "chrome 54", "not IE 11", "Safari > 11, iOS > 11" ]
> ./build.js { errors: [], warnings: [] } var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; (() => __async(this, null, function* () { const response = yield fetch("https://api.github.com/users/yaakaito"); console.log(response); }))();
yield
を使った結果に変化しました。簡単にではありますが、browserslist から esbuild の設定ができるようになりました。
おわりに
なんとなく設定していた browserslist ですが、最初から見直してみることで考えられる活用の幅が広がったように思います。 今回は、すでに掲載している推奨環境からクエリを逆算しましたが、関係を入替えて browserslist から推奨ブラウザを生成し更新を半自動化するような運用も考えられそうです。
明日は @rry です、お楽しみに!