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で使用できます:
- パッケージ・バンドラの使用。たとえば、複数のNPMパッケージを結合して単一のJavaScriptソース・ファイルにする場合です。
- ローカル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の組込みモジュール(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'組込みモジュールのロードがアプリケーションで試行されたときに、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');