GraalVM JavaScriptでのJavaScriptモジュールおよびパッケージの使用

GraalVM JavaScriptは最新のECMAScript標準と互換性があり、JavaベースのアプリケーションやNode.jsなどの様々な埋込みシナリオで実行できます。GraalVMのJavaScript埋込みシナリオに応じて、様々な方法でJavaScriptパッケージおよびモジュールを使用できます。

Node.js

GraalVMには、互換性のある特定のNode.jsバージョンが付属しています。したがって、アプリケーションは、サポートされているNode.jsバージョン(CommonJS、ESモジュール、ネイティブ・バインディングを使用するモジュールなど)と互換性のあるNPMパッケージを自由にインポートして使用できます。GraalVMでサポートされているNode.jsバージョンを確認および検証するには、bin/node --versionを実行するだけです。

Javaベースのアプリケーション(Context API)

(Context APIを使用して) Javaアプリケーションに埋め込むと、GraalVM JavaScriptは、Node.jsの組込みモジュール('fs''events''http'など)またはNode.js固有の関数(setTimeout()setInterval()など)に依存しないJavaScriptアプリケーションおよびモジュールを実行できます。これに対し、このようなNode.js組込みに依存するモジュールをGraalVMポリグロットContextにロードすることはできません。

サポートされているNPMパッケージは、次のいずれかの方法を使用してGraalVM JavaScript Contextで使用できます:

  1. パッケージ・バンドラの使用。たとえば、複数のNPMパッケージを結合して単一のJavaScriptソース・ファイルにする場合です。
  2. ローカルFileSystemでのESモジュールの使用。オプションで、カスタムTruffle FileSystemを使用して、ファイルの解決方法を構成できます。

デフォルトでは、Java Contextは、CommonJSのrequire()関数を使用したモジュールのロードをサポートしていません。これは、require()がNode.js組込み関数であり、ECMAScript仕様の一部ではないためです。CommonJSモジュールの試験段階のサポートは、次に説明するように、js.commonjs-requireオプションを使用して有効にできます。

ECMAScriptモジュール(ESM)

GraalVM JavaScriptは、import文、import()を使用したモジュールの動的インポート、および最上位レベルのawaitなどの高度な機能を含む、完全なESモジュール仕様をサポートしています。ECMAScriptモジュールは、モジュール・ソースを評価することでContextにロードできます。GraalVM JavaScriptでは、ファイル拡張子に基づいてECMAScriptモジュールがロードされます。したがって、ECMAScriptモジュールにはファイル名拡張子.mjsが必要です。または、モジュール・ソースのMIMEタイプが"application/javascript+module"である必要があります。

たとえば、次の単純なESモジュールを含むfoo.mjsという名前のファイルがあるとします:

export class Foo {

    square(x) {
        return x * x;
    }
}

このESモジュールは、次の方法でポリグロットContextにロードできます:

public static void main(String[] args) throws IOException {

    String src = "import {Foo} from '/path/to/foo.mjs';" +
                 "const foo = new Foo();" +
                 "console.log(foo.square(42));";

    Context cx = Context.newBuilder("js")
                .allowIO(true)
                .build();

	cx.eval(Source.newBuilder("js", src, "test.mjs").build());
}

ESモジュール・ファイルの拡張子は.mjsであることに注意してください。また、IOアクセスを有効にするためのallowIO()オプションが提供されていることにも注意してください。ESモジュールのその他の使用例については、ここを参照してください。

試験段階のモジュール・ネームスペース・エクスポート

試験段階の--js.esm-eval-returns-exportsオプションを使用すると、ESモジュール・ネームスペースをエクスポートしたオブジェクトをポリグロットのContextに公開できます。これは、Javaで直接ESモジュールを使用する場合に役立ちます:

public static void main(String[] args) throws IOException {

    String code = "export const foo = 42;";

    Context cx = Context.newBuilder("js")
                .allowIO(true)
                .option("js.esm-eval-returns-exports", "true")
                .build();

    Source source = Source.newBuilder("js", code)
                .mimeType("application/javascript+module")
                .build();

    Value exports = cx.eval(source);
    // now the `exports` object contains the ES module exported symbols.
    System.out.println(exports.getMember("foo").toString()); // prints `42`
}

このオプションはデフォルトでは無効です。

Truffle FileSystem

デフォルトでは、GraalVM JavaScriptは、ポリグロットContextの組込みFileSystemを使用してESモジュールをロードおよび解決します。FileSystemを使用して、ESモジュールのロード・プロセスをカスタマイズできます。たとえば、カスタムFileSystemを使用し、次のURLを使用してESモジュールを解決できます。

Context cx = Context.newBuilder("js").fileSystem(new FileSystem() {

	private final Path TMP = Paths.get("/some/tmp/path");

    @Override
    public Path parsePath(URI uri) {
    	// If the URL matches, return a custom (internal) Path
    	if ("http://localhost/foo".equals(uri.toString())) {
        	return TMP;
		} else {
        	return Paths.get(uri);
        }
    }

	@Override
    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
    	if (TMP.equals(path)) {
        	String moduleBody = "export class Foo {" +
                            "        square(x) {" +
                            "            return x * x;" +
                            "        }" +
                            "    }";
            // Return a dynamically-generated file for the ES module.
            return createByteChannelFrom(moduleBody);
        }
    }

    /* Other FileSystem methods not shown */

}).allowIO(true).build();

String src = "import {Foo} from 'http://localhost/foo';" +
             "const foo = new Foo();" +
             "console.log(foo.square(42));";

cx.eval(Source.newBuilder("js", src, "test.mjs").build());

この単純な例では、カスタムFileSystemを使用して、アプリケーションがhttp://localhost/foo URLのインポートを試みるときに動的に生成されるESモジュールをロードします。

ESモジュールをロードするためのカスタムTruffle FileSystemの完全な例は、ここを参照してください。

CommonJSモジュール(CJS)

デフォルトでは、Context APIではCommonJSモジュールがサポートされておらず、組込みrequire()関数もありません。JavaのContextからCommonJSモジュールをロードして使用するには、モジュールを自己完結型のJavaScriptソース・ファイルにバンドルする必要があります。これは、Parcel、Browserify、Webpackなど、多くの一般的なオープンソース・バンドル・ツールのいずれかを使用して行うことができます。CommonJSモジュールの試験段階のサポートは、次に説明するように、js.commonjs-requireオプションを使用して有効にできます。

Context APIでのCommonJS NPMモジュールの試験段階のサポート

js.commonjs-requireオプションを使用すると、組込みrequire()関数を通じてNPM互換のCommonJSモジュールをJavaScript Contextにロードできます。現在、これは試験段階の機能であり、本番用ではありません。

CommonJSサポートを有効にするには、次の方法でJavaScriptコンテキストを作成します:

Map<String, String> options = new HashMap<>();
// Enable CommonJS experimental support.
options.put("js.commonjs-require", "true");
// (optional) folder where the NPM modules to be loaded are located.
options.put("js.commonjs-require-cwd", "/path/to/root/folder");
// (optional) Node.js built-in replacements as a comma separated list.
options.put("js.commonjs-core-modules-replacements",
            "buffer:buffer/," +
            "path:path-browserify");
// Create context with IO support and experimental options.
Context cx = Context.newBuilder("js")
                            .allowExperimentalOptions(true)
                            .allowIO(true)
                            .options(options)
                            .build();
// Require a module
Value module = cx.eval("js", "require('some-module');");

"js.commonjs-require-cwd"オプションを使用すると、NPMパッケージがインストールされているメイン・フォルダを指定できます。たとえば、npm installコマンドが実行されたフォルダや、メインのnode_modulesフォルダが含まれるフォルダを指定します。NPMモジュールは、"js.commonjs-core-modules-replacements"を使用して指定された組込み置換を含め、そのフォルダを基準にして解決されます。

Node.jsの組込みrequire()関数との違い

Contextの組込みrequire()関数では、JavaScriptに実装されている通常のNPMモジュールはロードできますが、ネイティブNPMモジュールはロードできません。組込みrequire()FileSystemに依存するため、コンテキストの作成時にallowIOオプションを使用してI/Oアクセスを有効にする必要があります。組込みrequire()は、Node.jsとほぼ互換性を持つようにすることが目標とされており、ブラウザで動作するあらゆるNPMモジュール(たとえば、パッケージ・バンドラを使用して作成されたモジュール)と連携させることが想定されています。

Context APIを介して使用されるNPMモジュールのインストール

JavaScriptのContextからNPMモジュールを使用するには、モジュールをローカル・フォルダにインストールする必要があります。これは、Node.jsアプリケーションで通常行う場合と同様に、GraalVM JavaScriptのnpm installコマンドを使用して行うことができます。実行時に、オプションjs.commonjs-require-cwdを使用してNPMパッケージのメイン・インストール・フォルダを指定できます。組込みrequire()関数では、js.commonjs-require-cwdで指定されたディレクトリから始まるデフォルトのNode.jsのパッケージ解決プロトコルに従ってパッケージが解決されます。オプションでディレクトリを指定しない場合、アプリケーションの現在の作業ディレクトリが使用されます。

Node.jsコア・モジュールのモックアップ

一部のJavaScriptアプリケーションまたはNPMモジュールには、Node.jsの組込みモジュール(fsbufferなど)で使用可能な機能が必要になる場合があります。このようなモジュールは、Context APIでは使用できません。ありがたいことに、Node.jsコミュニティは、多くのNode.jsコア・モジュール(ブラウザ用のbufferモジュールなど)に対して高品質のJavaScript実装を開発しています。このような代替モジュール実装は、次のようにjs.commonjs-core-modules-replacementsオプションを使用してJavaScript Contextに公開できます:

options.put("js.commonjs-core-modules-replacements", "buffer:my-buffer-implementation");

コードに示されているように、このオプションでは、require('buffer')を使用したNode.js 'buffer'組込みモジュールのロードがアプリケーションで試行されたときに、my-buffer-implementationというモジュールをロードするようにGraalVM JavaScriptランタイムに指示します。

グローバル・シンボルの事前初期化

NPMモジュールまたはJavaScriptアプリケーションでは、特定のグローバル・プロパティがグローバル・スコープで定義されていると想定される場合があります。たとえば、アプリケーションまたはモジュールでは、JavaScriptグローバル・オブジェクトにBufferグローバル・シンボルが定義されていると想定される場合があります。このために、アプリケーション・ユーザー・コードはglobalThisを使用してアプリケーションのグローバル・スコープにパッチを適用できます:

// define an empty object called 'process'
globalThis.process = {};
// define the 'Buffer' global symbol
globalThis.Buffer = require('some-buffer-implementation').Buffer;
// import another module that might use 'Buffer'
require('another-module');