🥛

Scala.js の出力を CommonJS モジュールとして扱う

2021/07/04に公開

どうも、@armorik83 です。先日ひとつの案件が終わり、次の案件まで少しばかり休暇があったので、その隙にScala.jsの学習を進めていました。導入については前回の記事を参照

本稿では、前回の記事以降に解決したいくつかの疑問についてまとめます。

Q. Scala.js が出力する JS はとても読めない?

sbt fastOptJS

これで解決しました。前回はsbt fullOptJSの結果だけで判断してしまいましたが、あれは minify したのと同様にかなり圧縮されていただけで、デバッグ中はfastOptJSを使えばよさそう。同時に sourcemap も出力されますが、デバッガが変数の中身を表示しなくなるので個人的には使っていません。

10 行にも満たない.Scalaが 2000 行を超える.jsとして出力されますが、ランタイムは気合があれば読めます。

Q. JSApp を使わなければならない?

これは誤解でした。import scala.scalajs.js.JSAppして、JSAppを継承したobject内のmain()がブラウザでのロードに合わせて呼び出されるというだけで、ライブラリのように他の JS から呼ばれることを想定している場合は、必須ではありません。

http://www.scala-js.org/doc/export-to-javascript.html

package example

import scala.scalajs.js
import js.annotation.JSExport

@JSExport
object HelloWorld {
  @JSExport
  def main(): Unit = {
    println("Hello world!")
  }
}

このように@JSExportアノテーションを与えることで JS 側から呼び出せるコードとして出力されます。

Q. JSExport を CommonJS モジュールとして扱うには?

package pointPackage

import scala.scalajs.js
import js.annotation.JSExport
import scala.annotation.meta.field

@JSExport
class Point(
  @(JSExport @field) var x: Int,
  @(JSExport @field) var y: Int) {

  @JSExport
  def move(dx: Int, dy: Int) = {
    x += dx
    y += dy
  }
}

上のような(あまりよい例とは言えない)Scala ソースがあるとします。package 名は JS 変換時の対照を分かりやすくするためで、これもよい命名ではない。

これを次の JS ソースにて一旦ラップします。

point.js
var vm = require('vm');
var fs = require('fs');
var code = fs.readFileSync(__dirname + '/target/scala-2.11/point-fastopt.js').toString();
var sandbox = {};

vm.runInNewContext(code, sandbox, 'point-fastopt.js');
module.exports = sandbox.__ScalaJSExportsNamespace.pointPackage;

Scala.js の@JSExportは問答無用でglobalに出力されます。この出力先を制御するために__ScalaJSEnvを参照するようランタイムが組まれているのですが、その__ScalaJSEnv自体もグローバル変数のため、万が一 Scala.js 由来の JS ライブラリが混ざってしまったときに汚染が起こる恐れがあります。

vm.runInNewContextによって安全なスコープ内でグローバル展開ができるので、そこで Scala.js の Export を受け取り、sandbox.__ScalaJSExportsNamespace.pointPackageの箇所でその中身を CommonJS として Export しています。ES 2015 module として Export したければ、そのように書き換えるとよいでしょう。なお、このアイデアは mizchi 氏のものを拝借しています。(thx @mizchi!)

使うときは普通に npm モジュールを扱う感覚で OK。

es5.js
var Point = require('./point').Point;
var p = new Point(1, 2);
p.move(3, -5);

console.log('x:', p.x, 'y:', p.y);
// x: 4 y: -3

そりゃこれだけだったら全部 JS で書いたほうが早い。

Q. テストはどうするの?

テストは二種類実施するのがよいと捉えています。一つは Scala ソースとしてのテスト、これは JS 出力をせずに他の Scala と同じように行います。この辺のテスト事情はまだ探っている最中なので、本稿では解説しません。

もうひとつは e2e テスト。ここでいう e2e は DOM やブラウザを用いるテストではなく、プロダクトがきちんと JS 環境で動くかどうかのテストという意味です。ここでは JS の領域なのでMochaなどを使います。

簡単に試してみただけの印象ですが、@JSExport漏れで動かないケースがしばしばあったので、二度手間でも JS 側でのテストは必須でしょう。ただし API として公開している部分の検証のみで済むので、隠蔽できるロジックは Scala の単体テストのみでかまいません。


以上、しばらく遊んでます。

Discussion