Java でも ES2015 を使いたい! → (´・ω・`)


MOTIVATION

Babel は最新の ECMAScript 構文 (クラスやラムダや const, let など) を古い仕様の JS 環境でも実行できるように変換するトランスコンパイラ。例えば React は ECMA2015 + JSX のソースを Babel で一般的なブラウザでも解釈できる JS に変換しています。

React を使う用事があって、使い慣れた Finagle か Akka で API サーバを用意しようと思い、Node/React と JavaVM はうまく共存させたかったのがそもそもの始まり。Babel でブラウザ実行可能な JS が生成できるなら Java Scripting API の Nashorn で ES2015 が使えたり Node.js のライブラリが利用できるのでは? と。

CONCLUSION

結論から先に。

ES2015 で書かれた JS を Babel でトランスコンパイルして Nashorn で実行することは可能。ただし:

厳しい。理由は以下の通り。

  • babel-standalone の準備 eval()30 秒かかる。うなる CPU ファン。まぁサーバサイドなどは準備の済んだ ScriptEngine をキャッシュしておけば良いかもしれない。
  • 準備時間を妥協できるならトランスコンパイルは可能。ただ Nashorn には require() がなく、従って外部ライブラリが使えない。webpack も併せて使う必要がありそう。あるいは Java で同等機能を実装して bindings で渡せば回避可能かもしれない。
  • そもそも環境設定に npm を前提とするので Nashorn を使わずとも babel を直接起動する手段がある (そしてそっちの方が速い)。
  • babel-standalone は presets に env が使えないなど制約が厳しい。

従って Nashorn を使うより実行環境の node なり npm なりを Runtime.exec() で使用するほうが良い。

PLAN

こんなプランで実行しましたという手順。

npm で es2015 の presets をインストールしています (が、本当に必要だったかは不明)。

$ npm init
$ npm install babel-cli babel-preset-es2015 --save

1. babel.js の準備

babel-standaloneInstallation にあるようにリリースページや npm で babel.js を入手します。

final ScriptEngineManager manager = new ScriptEngineManager();

final ScriptEngine babel = manager.getEngineByName("JavaScript");
final String babelJS = "babel.js";
babel.put(ScriptEngine.FILENAME, babelJS);
try(Reader in = new FileReader(babelJS)){
  babel.eval(in);
}

この babel.eval(in) で 30 秒程度かかります。また babel.min.js を使用すると例外が発生しますので babel.js を使用します。

javax.script.ScriptException: SyntaxError: empty range in char class in babel.min.js at line number 4
        at jdk.scripting.nashorn/jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(Unknown Source)
...
Caused by: babel.min.js:4 SyntaxError: empty range in char class
        at jdk.scripting.nashorn/jdk.nashorn.internal.runtime.ECMAErrors.error(Unknown Source)
...
Caused by: jdk.nashorn.internal.runtime.ParserException: empty range in char class
        at jdk.scripting.nashorn/jdk.nashorn.internal.runtime.regexp.RegExp.throwParserException(Unknown Source)
...

2. トランスコンパイルの実行

final String es2015JS = "es2015.js";
final String es2015 = new String(Files.readAllBytes(Paths.get(es2015JS)), StandardCharsets.UTF_8);
babel.put(ScriptEngine.FILENAME, "<transcompile>");
babel.put("src", es2015);
babel.put("a", new Object[3]);
final Object[] result = (Object[])babel.eval(
  "var r = Babel.transform(src, {presets:['es2015']});\n" +
  "a[0] = r.code;\n" +
  "a[1] = r.map;\n" +
  "a[2] = r.ast;\n" +
  "a"
);
System.out.println(result[0]);

Babel.transformFileSync は何故か undefined でした。関数内で arguments とか使っているから?

とりあえず一度 ES2015 のソースを読み込んで transform し結果をリターンバッファに格納します。es2015.js の中は以下の通り:

// run `npm install kuromoji` before
import kuromoji from "kuromoji"

kuromoji.builder({ dicPath: "./node_modules/kuromoji/dict" }).build((err, tokenizer) => {
  var path = tokenizer.tokenize("すもももももももものうち")
  console.log(path)
})

Nashorn 上でのトランスコンパイルにより以下のようなソースが生成されました。

"use strict";

var _kuromoji = require("kuromoji");

var _kuromoji2 = _interopRequireDefault(_kuromoji);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

_kuromoji2.default.builder({ dicPath: "./node_modules/kuromoji/dict" }).build(function (err, tokenizer) {
  var path = tokenizer.tokenize("すもももももももものうち");
  console.log(path);
}); // run `npm install kuromoji` before

Babel でトランスコンパイルした結果も全く同じ。

$ node_modules/.bin/babel --presets=es2015 es2015.js

3. トランスコンパイルされたコードの実行

スクリプトエンジンを Babel とは別にしてトランスコンパイルされたコードを実行します。

final ScriptEngine engine = manager.getEngineByName("JavaScript");
engine.put(ScriptEngine.FILENAME, es2015JS);
engine.eval(result[0].toString());

しかし require() を使っているので実行できませんね

javax.script.ScriptException: ReferenceError: "require" is not defined in es2015.js at line number 3
        at jdk.scripting.nashorn/jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(Unknown Source)
...

おしまし