12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-10-23

はじめに

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 を元に作られたというだけあって,同じように使えることが分かった。

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

Gemfile
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 で分かる。

12
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?