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');