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 で実行することは可能。ただし:

<img src="https://qiita-image-store.s3.amazonaws.com/0/2287/aa312adb-aa69-07a3-5e6e-2a170be4d8be.jpeg" width="300" float="right"/>

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

* 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-standalone](https://github.com/babel/babel/tree/master/packages/babel-standalone) の [Installation](https://github.com/babel/babel/tree/master/packages/babel-standalone#installation) にあるように[リリースページ](https://github.com/babel/babel-standalone/releases)や npm で `babel.js` を入手します。

```java
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. トランスコンパイルの実行

```java
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 の中は以下の通り:

```javascript
// 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 上でのトランスコンパイルにより以下のようなソースが生成されました。

```javascript
"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 とは別にしてトランスコンパイルされたコードを実行します。

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

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

```
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)
...
```

おしまし