GraalJSにおけるJavaScriptモジュールとパッケージの使用

GraalJSは最新のECMAScript標準と互換性があり、さまざまなJavaベースの埋め込みシナリオで実行できます。埋め込みに応じて、JavaScriptパッケージとモジュールは異なる方法で使用される場合があります。

Context APIによるJava埋め込み #

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

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

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

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

ECMAScriptモジュール(ESM) #

GraalJSは、importステートメント、import()を使用した動的モジュールインポート、およびトップレベルのawaitなどの高度な機能を含む、完全なESモジュール仕様をサポートしています。

ECMAScriptモジュールは、モジュールソースを評価するだけでContextにロードできます。GraalJSは、ファイル拡張子に基づいてECMAScriptモジュールをロードします。したがって、すべてのECMAScriptモジュールには、ファイル名拡張子.mjsが必要です。または、モジュールSourceの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オプション(デフォルトではfalse)を使用すると、ESモジュール名前空間のエクスポートオブジェクトをポリグロットContextに公開できます。これは、ESモジュールがJavaから直接使用される場合に便利です。

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 #

デフォルトでは、GraalJSはポリグロット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());

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

ESモジュールをロードするためのカスタムTruffle FileSystemの完全な例は、こちらにあります。

CommonJSモジュール #

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

Context APIでのCommonJS NPMモジュールの実験的なサポート

js.commonjs-requireオプションは、JavaScript ContextでNPM互換のCommonJSモジュールをロードするために使用できる組み込みのrequire()関数を提供します。現在、これは実験的な機能であり、本番環境での使用には適していません。

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

Map<String, String> options = new HashMap<>();
// Enable CommonJS experimental support.
options.put("js.commonjs-require", "true");
// (optional) directory where the NPM modules to be loaded are located.
options.put("js.commonjs-require-cwd", "/path/to/root/directory");
// (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/ディレクトリを含むディレクトリにすることができます。"js.commonjs-core-modules-replacements"を使用して指定された組み込みの代替を含め、すべてのNPMモジュールは、そのディレクトリを基準に解決されます。

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

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

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

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

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

一部のJavaScriptアプリケーションまたはNPMモジュールでは、Node.jsの組み込みモジュール(たとえば、'fs''buffer')で利用可能な機能が必要になる場合があります。このようなモジュールは、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組み込みモジュールをロードしようとしたときに、GraalJSにmy-buffer-implementationというモジュールをロードするように指示します。

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

NPMモジュールまたはJavaScriptアプリケーションでは、特定のグローバルプロパティがグローバルスコープで定義されていることを期待する場合があります。たとえば、アプリケーションまたはモジュールでは、BufferグローバルシンボルがJavaScriptグローバルオブジェクトで定義されていることを期待する場合があります。この目的のために、アプリケーションユーザーコードは、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');

私たちとつながる