JSweet を利用して Webpack で Java コードを利用する

この投稿で解決したい状況として、 Java コードで書かれた処理をどうしても JavaScript から利用したい場合があります。コードと表現したのは、コンパイルして .jar になったものでなく、ソースコードとして手元にあるという意味です。

今回は、 JSweet というツールを利用します。これをただ簡単に利用するだけなら特に難しくありません。

サンプルプロジェクトはこちら

やること

とあるプロジェクトの構成として、以下のファイルがあるとします。

JavaScript での依存関係を表す require と間違えて処理されないよう、この記事で Java コードを呼び出す場合は const {Example} = require("java-code-class?bar.Example") と呼び出すことにします。

解決法

この制約を実現する処理は、 Java コードを JavaScript コードへ変換するものと、 JavaScript からの呼び出しを生成ファイルのパスとして解決するものから成り立っています。
今回は、前者を JSweet のコマンドラインの watch と webpack-serve で、後者を Webpack の resolve の Plugin を作成することで実現します。
Java コードを編集してリロードされるまでに 1s 未満のタイムラグはありますが、概ね Java コードを修正したらすぐに動作に変化が生じます。

Java to JavaScript 変換

この記事では、 Java コードを JavaScript コードに変換するところで、一旦 Java コード を TypeScript コードに変換し、さらに TypeScript コードを JavaScript コードにコンパイルするような手段をとります。
一度 TypeScript に変換する利点として、型など情報量を多く保持でき、設定の自由度も上がることがあります。

JSweet 設定

まずは JSweet を使えるようにする必要があります。
http://repository.jsweet.org/artifactory/libs-release-local/org/jsweet/ より、 “jsweet-core” と “jsweet-transpiler with-dependencies” の最新版の .jar をダウンロードしてローカルに保存してください。(執筆時の最新版は、それぞれ “5-201726” と “2.0.1” です。)
続いて、 JavaScript の spawn により JSweet トランスパイラを動かします。( “node-java” を使った際は、ログ出力の取扱いが面倒でした。)
‘src/java/main’ を入力元となるディレクトリとし、 commonjs 形式の TypeScript コードだけを “tmp/jsweet” 以下へ出力するように設定します。(デフォルトでは JavaScript コードも出力されます。)

watch.js
8
9
10
11
12
13
// This is for Windows
const jsweetProcess = spawn('java', ['-cp', 'dev/jsweet-core-5-20170726.jar;dev/jsweet-transpiler-2.0.1-jar-with-dependencies.jar',
'org.jsweet.JSweetCommandLineLauncher', '--input', './src/java/main',
'--module', 'commonjs', '--tsOnly', '--tsout', JSWEET_OUTPUT_DIR, '--watch'], {
shell: true
}); // const JSWEET_OUTPUT_DIR = "tmp/jsweet";

サンプルプロジェクトでは、 yarn start build-watch というコマンドで実行するようになっています。 これで、 ‘src/java/main/bar/Example.java’ を元にした ‘tmp/jsweet/bar/Example.ts’ が生成されているのが確認できるはずです。

TypeScript ビルド設定

それでは、 ‘webpack.config.dev.js’ の設定に移ります。
今回は無難に “ts-loader” を通してビルドしますが、設定の面白みはないので軽くだけ紹介します。

  • webpack.config.dev.js “module.rules” へ、 ‘tmp/jsweet’ 以下の TypeScript ファイルを対象に “ts-loader” を指定し、さらに “resolve.extensions” に “.ts” と加えています。
  • tsconfig.json JSweet が生成する TypeScript コードの最終行をエラーにしないよう “suppressImplicitAnyIndexErrors” を入れた以外、なんの変哲もない設定です。

パス解決

ここで問題になるのは、どうやって Java コードのロジックを require するかです。
冒頭にあるように、この記事では require("java-code-class?bar.Example") で利用できると決めました。
これをただ Webpack に通したのでは “java-code-class” というモジュール or ファイルが ‘node_modules’ 以下に無いというエラーが出てうまくいきません。

今までの記事にあったような loader を通して require 内の文字列を置き換えるのも一つの手ですが、今回の状況では利用側のコードが変更されるわけではないので、 loader による解決は不自然です。

Webpack の Resolve の Plugin

この状況では、パス解決方法を設定するのがより自然に感じます。
Webpack の “resolve” の設定でそれが行なえますが、用意された機能では不十分なため、 Plugin で対応することとします。( Webpack の設定

よくある Webpack Plugin と同じく、こちらもドキュメントがほとんどありません。
config の “resolve.plugins” に加えるのは enhanced-resolve に指定する Plugin と同一っぽいので、ここのソースを参考にしながら実装することになります。

手探りになりましたが、いくつかの Hook ポイントを経て “described-resolve” という Hook ポイントに入ったリクエストの中から、上記の “java-code-class” と同一だったらリクエストを改変して “raw-file” という Hook ポイントに送る、というフローを新たに加えるものを実装しました。
これを “JavaCodeResolvePlugin” と名付けて、先ほど生成された TypeScript ファイルの場所を引数に持たせたプラグインとします。

config ファイルにはこのように設定します。

webpack.config.dev.js
25
26
27
28
29
30
31
32
33
resolve: {
extensions: [
'.js',
'.ts'
],
plugins: [
new JavaCodeResolvePlugin({targetRoot: path.resolve(__dirname, 'tmp/jsweet')})
]
},

こうした設定では、 require("java-code-class?bar.Example") が ‘(abs path)/tmp/jsweet/bar/Example’ と解釈されます。(続いて、下流で ‘(abs path)/tmp/jsweet/bar/Example.js’ 、 ‘(abs path)/tmp/jsweet/bar/Example.ts’ と順番に確認されます。)

動作確認

yarn run dev を実行すると、いつもどおり webpack-serve のサーバーが立ち上がります。
その状態のまま yarn run build-watch を実行して、 JSweet の watch も開始すると、準備はすべて完了です。

webpack-serve のサーバーの URL を開き、ブラウザの開発者画面のコンソールタブを見てみると、最初は 1 2 とそれぞれ表示されています。
ここから Java コードの getter の中身や JavaScript コードのコンストラクタへの引数を変更するとページが再読込され、 webpack-serve を再起動せずとも表示される数字を変化させられます。

まとめ

  • JSweet と Webpack の “ts-loader” を使い、 Java コードで書かれたロジックを JavaScript コードから呼び出す例を紹介しました。
  • JSweet の watch モードと webpack-serve により、 Java コードの変更を再起動なしに反映させられます。
  • Java コードの呼び出しとそれ以外を区別する用途で、 Webpack の resolve の Plugin を新しく作って適用する例を紹介しました。