Ruby での JavaScript の圧縮を Uglifier から Terser に変えた

# はじめに

JavaScript を Ruby で圧縮したい。
こういう処理はフレームワーク側でやってくれるので，あまり意識することはないと思うが，たとえば静的サイトを生成するプログラムを自分で書く場合，やり方を知らなければならない。

# 定番 Uglifier

従来，こういう目的には [Uglifier](https://github.com/lautis/uglifier) という gem がよく使われてきた。

執筆時点の最新版は 4.2.0 なので，本記事もこれを前提とする。

使い方は超簡単で，

```rb
require "uglifier"

puts Uglifier.compile(<<~JS)
  function average(numbers) {
    return numbers.reduce(
      function(sum, number) { return sum + number },
      0
    ) / numbers.length
  }
JS
```

とすれば，ヒアドキュメントで書いた JavaScript を圧縮した

```
function average(e){return e.reduce(function(e,n){return e+n},0)/e.length}
```

が表示された。
改行・インデントが抑制されただけでなく，引数名が 1 文字になったりしている。
うむうむ，いいぞ。

しかし，あれだね，上のサンプルコードだとアロー関数とか使ってなくて，「旧石器時代の JavaScript かよ」とか言われそう。

はいはい，分かりました。ではイマドキの例を。

```rb
require "uglifier"

puts Uglifier.compile(<<~JS)
  function average(numbers) {
    return numbers.reduce(
      (sum, number) => sum + number,
      0
    ) / numbers.length
 }
JS
```

結果：

```
`parse_result': Invalid assignment (Uglifier::Error)
```

な，なんかエラーになったんだが。

どうやら ES6 に対応させるには　`harmony: true` というオプションを与えなければならないようだ。

これでどうだ：

```rb
require "uglifier"

puts Uglifier.compile(<<~JS, harmony: true)
  function average(numbers) {
    return numbers.reduce(
      (sum, number) => sum + number,
      0
    ) / numbers.length
  }
JS
```

結果：

```
function average(e){return e.reduce((e,n)=>e+n,0)/e.length}
```

おーけー，おーけー。

と，ここまでは実は前から知ってたんだけど，先日困ったことが起こった。
[Stimulus](https://stimulus.hotwired.dev/) を使った静的サイトを書いてて，いつものように Uglifier で圧縮してたんだけど，JavaScript に少し加筆したところで Uglifier がまたエラーを出した。
「おっと書き間違えたか」と思ったけど間違ってはなかった。

はて？と調べてみると，そもそも Uglifier の `harmony` オプションは実験的なもので，ES6 に完全に対応しているわけではなさそうだった。

改めて [Uglifier のリポジトリー](https://github.com/lautis/uglifier) を見に行くと，Issues も Pull requests も溜まってて，開発が停止していることがわかった。
どうやら代替 gem を探さなければならないようだ。

# Terser

見つかったのがコレ。
https://github.com/ahorek/terser-ruby

えっと，まず，Ruby とは関係なしに [Terser](https://github.com/terser/terser) という「JavaScript で書かれた JavaScript 圧縮器」があり，それのいわゆるラッパーとして terser という同名の gem がある。
元の Terser と区別するため，gem のほうは「terser-ruby」と呼ばれるようだ。リポジトリー名もそうなっている（しかし gem 名はあくまで terser）。

実は Uglifier の（本記事執筆時点での）[README](https://github.com/lautis/uglifier/blob/master/README.md) には

> ES6 を圧縮するんなら ruby-terser がより良い選択肢だよ

って書いてあるんですな，これが。
ん？　terser-ruby か ruby-terser かどっちやねん？　まあ本家が terser-ruby ってなってるから，Uglifier の README 書いた人が間違えたんだろうね，たぶん。

はー，でもまた新しいライブラリーの使い方を学ばなくてはならんのかー，と思ったら，Uglifier を元に作られたというだけあって，同じように使えることが分かった。

基本的な使い方は以下のとおり：

```rb:Gemfile
source "https://rubygems.org"

gem "terser"
```

```rb
require "bundler"
Bundler.require

puts Terser.compile(<<~JS)
  function average(numbers) {
    return numbers.reduce(
      (sum, number) => sum + number,
      0
    ) / numbers.length
  }
JS
```

```:結果
function average(e){return e.reduce(((e,n)=>e+n),0)/e.length}
```

うむ。いいね。
Terser はもともと ES6 に対応しているので，Uglifier にあった `harmony` というオプションは無い。

ちなみに，README を見れば分かるとおり，オプションは豊富にある。
たとえばデフォルトでは出力が ASCII 文字だけで表現される設定なので，文字列の `"ほげ"` が `"\u307b\u3052"` になってしまう。この設定を解除するには，

```rb
Terser.compile(javascript, output: {ascii_only: false})
```

みたいにすればいい。



# 注意点

実はすんなりと上のように解決したわけではなかった。
途中でちょっとしたトラブルがあった。
内部で利用される JavaScript 処理系によってはエラーが出るのだ。

ええと，terser-ruby が利用している Terser という JavaScript プログラムは，JavaScript であるからして，実行するためには当然 JavaScript の処理系が必要になるよね。
terser-ruby はその処理系を内蔵しておらず，JavaScript の実行は [ExecJS](https://github.com/rails/execjs) という gem に任せている。

しかし，ExecJS も，それ自身は処理系を内蔵しておらず，さまざまな形で提供されるさまざまな JavaScript 処理系を利用するための gem であるようだ。
つまり，ExecJS はユーザープログラムと多様な JavaScript 処理系の橋渡しが役目であるらしい。

[therubyracer](https://github.com/rubyjs/therubyracer)（開発停止） とか [mini_racer](https://github.com/rubyjs/mini_racer) とか [duktape](https://github.com/judofyr/duktape.rb) といった gem 名を目にしたことがあるかもしれない。これらは JavaScript 処理系を提供する gem らしい。
ExecJS はこれら（のどれか）を利用して JavaScript コードを実行する。
使う処理系は gem として提供されているもの以外にも対応しているのだが，私はあまりよく知らないので，割愛。

前置きが長くなったが，私が遭遇したトラブルというのは，ExecJS が duktape を使う場合，Terser が JavaScript コードをパースするときに

```
parse error (line 443) (Duktape::SyntaxError)
```

というエラーが起きる，というもの。このエラーそのものはあまり追究しなかった。

ExecJS が JavaScript 処理系をどういうルールで選ぶのかよく分からないんだけど，もともと Gemfile に

```rb
gem "duktape"
```

を入れていたため，duktape が採用されたらしい。
この行を削除したら（どの処理系が選ばれたのかは分からないけど）前節で書いたようにうまく動作した。

Bundler を使わないで，

```rb
require "terser"
Terser.compile("const a = 1")
```

とやった場合も（私の環境では）duktape が選ばれ，エラーが出た。
うーむ。

## 追記（2021-11-07）

ExecJS が JavaScript 処理系（ランタイム）をどのように選ぶかについて，以下の記事が投稿された（@kyntk さんありがとう）。
[ExecJSが自動で選択するランタイムはどのように決まるのか - Qiita](https://qiita.com/kyntk/items/07a8ea95f32b761c2f3c)

これによれば，環境変数 `EXECJS_RUNTIME` で制御することも可能。
実際に選ばれたランタイムの名前は `ExecJS.runtime.name` で分かる。