Electron での ES Modules (ESM)
はじめに
ECMAScript Module (ESM) 形式は、JavaScript パッケージの標準的な読み込み方法 です。
Chromium と Node.js にはそれら独自の ESM 仕様の実装があり、Electron はコンテキストに応じて使用するモジュール ローダーを選択します。
このドキュメントでは、Electron の ESM における制限、及び Electron の ESM と Node.js や Chromium の ESM との違いの概要を説明します。
この機能は electron@28.0.0
で追加されました。
概要: ESM のサポートマトリックス
この表には、ESM がサポートされる場所と、どの ESM ローダーが使用されるかについての概要を示します。
プロセス | ESM ローダー | プリロードでの ESM ローダー | 適用可能となる要件 |
---|---|---|---|
メイン | Node.js | なし | |
レンダラー (サンドボックス化) | Chromium | 未サポート | |
レンダラー (非サンドボックス化 & コンテキスト隔離) | Chromium | Node.js | |
レンダラー (非サンドボックス化 & コンテキスト隔離なし) | Chromium | Node.js |
メインプロセス
Electron のメインプロセスは Node.js のコンテキストで実行され、その ESM ローダーを使用します。 使用方法は Node の ESM のドキュメント に従ってください。 メインプロセスのファイルで ESM を有効化するには、次の条件のいずれかを満たす必要があります。
- ファイル名が
.mjs
の拡張子で終わること。 - 最も近い親パッケージの package.json に
"type": "module"
が設定されていること
詳細については、Node の モジュールシステムの決定 のドキュメントをご参照ください。
Caveats
アプリの ready
イベントの前でしらみつぶしに await
を使用する必要があります
ES Modules は 非同期に ロードされます。 これは、メインプロセスのエントリポイントでのインポートによる副作用は、ready
イベントより前のときにのみ実行されることを意味します。
This is important because certain Electron APIs (e.g. app.setPath
)
need to be called before the app's ready
event is emitted.
Node.js ESM ではトップレベルの await
が利用できます。これを使用して、ready
イベントより前に実行する必要があるすべての Promise を必ず await
してください。 さもなくば、あなたのコードが実行される前に ready
になってしまうかもしれません。
これは特に、動的な ESM インポート文の場合の留意が重要になります (静的インポートには影響しません)。
たとえば、index.mjs
がトップレベルで import('./set-up-paths.mjs')
を呼び出す場合、動的インポートが解決されるまでの間でアプリはすでに ready
になっている可能性があります。
// この呼び出しに await を追加して、`ready` より前にパスのセットアップが完了することを保証してください。
import('./set-up-paths.mjs')
app.whenReady().then(() => {
console.log('This code may execute before the above import')
})
JavaScript トランスパイラー (Babel、TypeScript など) は、Node.js が ESM インポートをサポートするよりも前に、これらの呼び出しを CommonJS の require
呼び出しに変換することで、歴史的に ES Module 構文をサポートしてきました。例: @babel/plugin-transform-modules-commonjs
@babel/plugin-transform-modules-commonjs
プラグインは、ESM のインポートを require
呼び出しに変換します。 正確な構文は importInterop
の設定 に依存します。import foo from "foo";
import { bar } from "bar";
foo;
bar;
// "importInterop: node" を付けると、以下にコンパイルされます...
"use strict";
var _foo = require("foo");
var _bar = require("bar");
_foo;
_bar.bar;
これらの CommonJS 呼び出しは、モジュールコードを同期的に読み込みます。 CJS コードへのトランスパイルをネイティブ ESM に移行する場合は、CJS と ESM とのタイミングの違いにご注意ください。
レンダラープロセス
Electron のレンダラープロセスは Chromium コンテキストで実行され、Chromium の ESM ローダーを使用します。
実際には、これは以下のような import
文のことです。
- Node.js 組み込みモジュールへのアクセス権限はありません
node_modules
から npm パッケージを読み込めません
<script type="module">
import { exists } from 'node:fs' // ❌ 動作しません!
</script>
npm 経由で JavaScript パッケージをレンダラープロセスへ直接読み込ませたい場合は、webpack や Vite などのバンドラーを使用して、クライアント側で使用できるようにコードをコンパイルすることをお勧めします。
プリロードスクリプト
レンダラーのプリロードスクリプトは、利用できれば Node.js の ESM ローダーを使用します。
ESM が利用できるかどうかは、レンダラーの sandbox
および contextIsolation
の設定値によって決まります。また、ESM 読み込みの非同期的な性質に起因するその他の注意事項がいくつかあります。
Caveats
ESM のプリロードスクリプトは拡張子が .mjs
でなければなりません
プリロードスクリプトは "type": "module"
フィールドを無視するため、ESM のプリロード スクリプトでは .mjs
ファイル拡張子を使用 しなければなりません。
サンドボックス化されたプリロードスクリプトは ESM インポートを使用できません
サンドボックス化されたプリロードスクリプトは、ESM のコンテキストがないプレーンな JavaScript として実行されます。 外部モジュールを使用する必要がある場合は、プリロードのコードにバンドラを使用することをお勧めします。 electron
API の読み込みは、引き続き require('electron')
を介して行われます。
For more information on sandboxing, see the Process Sandboxing docs.
サンドボックス化されていない ESM のプリロードスクリプトは、コンテンツがないページでページを読み込んだ後に実行されます
レンダラーが読み込んだページのレスポンス本文が 完全に 空 (つまり Content-Length: 0
) の場合、プリロードスクリプトがページのロードをブロックしないため、競合状態が発生する可能性があります。
この影響がある場合は、応答本文に 何か を含めるように変更するか (例えば空の html
タグ (<html></html>
))、CommonJS のプリロードスクリプト (.js
や .cjs
) を使用するように差し戻してください。これにより、ページの読み込みがブロックされます。
動的な Node.js の ESM インポートを使用するには、ESM プリロードスクリプトをコンテキスト隔離する必要があります
サンドボックス化されていないレンダラープロセスで contextIsolation
フラグが有効になっていない場合、Node の ESM ローダーを介してファイルを動的に import()
することはできません。
// ❌ これらはコンテキスト隔離なしでは動作しません
const fs = await import('node:fs')
await import('./foo')
これは、レンダラープロセスでは Chromium の動的な ESM の import()
関数が通常は優先されて、コンテキスト隔離がないと Node.js が動的なインポート文を利用できるかを知る方法がないためです。 コンテキスト隔離を有効にすると、レンダラーでの隔離プリロードのコンテキストからの import()
文を Node.js のモジュールローダーへ転送できます。