はじめに
JavaScript を Ruby で圧縮したい。
こういう処理はフレームワーク側でやってくれるので,あまり意識することはないと思うが,たとえば静的サイトを生成するプログラムを自分で書く場合,やり方を知らなければならない。
定番 Uglifier
従来,こういう目的には Uglifier という gem がよく使われてきた。
執筆時点の最新版は 4.2.0 なので,本記事もこれを前提とする。
使い方は超簡単で,
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 かよ」とか言われそう。
はいはい,分かりました。ではイマドキの例を。
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
というオプションを与えなければならないようだ。
これでどうだ:
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 を使った静的サイトを書いてて,いつものように Uglifier で圧縮してたんだけど,JavaScript に少し加筆したところで Uglifier がまたエラーを出した。
「おっと書き間違えたか」と思ったけど間違ってはなかった。
はて?と調べてみると,そもそも Uglifier の harmony
オプションは実験的なもので,ES6 に完全に対応しているわけではなさそうだった。
改めて Uglifier のリポジトリー を見に行くと,Issues も Pull requests も溜まってて,開発が停止していることがわかった。
どうやら代替 gem を探さなければならないようだ。
Terser
見つかったのがコレ。
https://github.com/ahorek/terser-ruby
えっと,まず,Ruby とは関係なしに Terser という「JavaScript で書かれた JavaScript 圧縮器」があり,それのいわゆるラッパーとして terser という同名の gem がある。
元の Terser と区別するため,gem のほうは「terser-ruby」と呼ばれるようだ。リポジトリー名もそうなっている(しかし gem 名はあくまで terser)。
実は Uglifier の(本記事執筆時点での)README には
ES6 を圧縮するんなら ruby-terser がより良い選択肢だよ
って書いてあるんですな,これが。
ん? terser-ruby か ruby-terser かどっちやねん? まあ本家が terser-ruby ってなってるから,Uglifier の README 書いた人が間違えたんだろうね,たぶん。
はー,でもまた新しいライブラリーの使い方を学ばなくてはならんのかー,と思ったら,Uglifier を元に作られたというだけあって,同じように使えることが分かった。
基本的な使い方は以下のとおり:
source "https://rubygems.org"
gem "terser"
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"
になってしまう。この設定を解除するには,
Terser.compile(javascript, output: {ascii_only: false})
みたいにすればいい。
注意点
実はすんなりと上のように解決したわけではなかった。
途中でちょっとしたトラブルがあった。
内部で利用される JavaScript 処理系によってはエラーが出るのだ。
ええと,terser-ruby が利用している Terser という JavaScript プログラムは,JavaScript であるからして,実行するためには当然 JavaScript の処理系が必要になるよね。
terser-ruby はその処理系を内蔵しておらず,JavaScript の実行は ExecJS という gem に任せている。
しかし,ExecJS も,それ自身は処理系を内蔵しておらず,さまざまな形で提供されるさまざまな JavaScript 処理系を利用するための gem であるようだ。
つまり,ExecJS はユーザープログラムと多様な JavaScript 処理系の橋渡しが役目であるらしい。
therubyracer(開発停止) とか mini_racer とか duktape といった gem 名を目にしたことがあるかもしれない。これらは JavaScript 処理系を提供する gem らしい。
ExecJS はこれら(のどれか)を利用して JavaScript コードを実行する。
使う処理系は gem として提供されているもの以外にも対応しているのだが,私はあまりよく知らないので,割愛。
前置きが長くなったが,私が遭遇したトラブルというのは,ExecJS が duktape を使う場合,Terser が JavaScript コードをパースするときに
parse error (line 443) (Duktape::SyntaxError)
というエラーが起きる,というもの。このエラーそのものはあまり追究しなかった。
ExecJS が JavaScript 処理系をどういうルールで選ぶのかよく分からないんだけど,もともと Gemfile に
gem "duktape"
を入れていたため,duktape が採用されたらしい。
この行を削除したら(どの処理系が選ばれたのかは分からないけど)前節で書いたようにうまく動作した。
Bundler を使わないで,
require "terser"
Terser.compile("const a = 1")
とやった場合も(私の環境では)duktape が選ばれ,エラーが出た。
うーむ。
追記(2021-11-07)
ExecJS が JavaScript 処理系(ランタイム)をどのように選ぶかについて,以下の記事が投稿された(@kyntk さんありがとう)。
ExecJSが自動で選択するランタイムはどのように決まるのか - Qiita
これによれば,環境変数 EXECJS_RUNTIME
で制御することも可能。
実際に選ばれたランタイムの名前は ExecJS.runtime.name
で分かる。