メインコンテンツへ飛ぶ

Electron での ES Modules (ESM)

はじめに

ECMAScript Module (ESM) 形式は、JavaScript パッケージの標準的な読み込み方法 です。

Chromium と Node.js にはそれら独自の ESM 仕様の実装があり、Electron はコンテキストに応じて使用するモジュール ローダーを選択します。

このドキュメントでは、Electron の ESM における制限、及び Electron の ESM と Node.js や Chromium の ESM との違いの概要を説明します。

info

この機能は electron@28.0.0 で追加されました。

概要: ESM のサポートマトリックス

この表には、ESM がサポートされる場所と、どの ESM ローダーが使用されるかについての概要を示します。

プロセスESM ローダープリロードでの ESM ローダー適用可能となる要件
メインNode.jsなし
レンダラー (サンドボックス化)Chromium未サポート
レンダラー (非サンドボックス化 & コンテキスト隔離)ChromiumNode.js
レンダラー (非サンドボックス化 & コンテキスト隔離なし)ChromiumNode.js

メインプロセス

Electron のメインプロセスは Node.js のコンテキストで実行され、その ESM ローダーを使用します。 使用方法は Node の ESM のドキュメント に従ってください。 メインプロセスのファイルで ESM を有効化するには、次の条件のいずれかを満たす必要があります。

  • ファイル名が .mjs の拡張子で終わること。
  • 最も近い親パッケージの package.json に "type": "module" が設定されていること

詳細については、Node の モジュールシステムの決定 のドキュメントをご参照ください。

Caveats

アプリの ready イベントの前でしらみつぶしに await を使用する必要があります

ES Modules は 非同期に ロードされます。 これは、メインプロセスのエントリポイントでのインポートによる副作用は、ready イベントより前のときにのみ実行されることを意味します。

特定の Electron API (例: app.setPath) はアプリの ready イベントが発行される に呼び出す必要があるため、これは重要です。

Node.js ESM ではトップレベルの await が利用できます。これを使用して、ready イベントより前に実行する必要があるすべての Promise を必ず await してください。 さもなくば、あなたのコードが実行される前に ready になってしまうかもしれません。

This is particularly important to keep in mind for dynamic ESM import statements (static imports are unaffected). たとえば、index.mjs がトップレベルで import('./set-up-paths.mjs') を呼び出す場合、動的インポートが解決されるまでの間でアプリはすでに ready になっている可能性があります。

index.mjs (Main Process)
// この呼び出しに 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 構文をサポートしてきました。

Example: @babel/plugin-transform-modules-commonjs

@babel/plugin-transform-modules-commonjs プラグインは、ESM のインポートを require 呼び出しに変換します。 正確な構文は importInterop の設定 に依存します。

@babel/plugin-transform-modules-commonjs
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') を介して行われます。

サンドボックス化についてのさらなる情報は、プロセスのサンドボックス化 のドキュメントをご参照ください。

サンドボックス化されていない ESM のプリロードスクリプトは、コンテンツがないページでページを読み込んだ後に実行されます

レンダラーが読み込んだページのレスポンス本文が 完全に 空 (つまり Content-Length: 0) の場合、プリロードスクリプトがページのロードをブロックしないため、競合状態が発生する可能性があります。

この影響がある場合は、応答本文に 何か を含めるように変更するか (例えば空の html タグ (<html></html>))、CommonJS のプリロードスクリプト (.js.cjs) を使用するように差し戻してください。これにより、ページの読み込みがブロックされます。

動的な Node.js の ESM インポートを使用するには、ESM プリロードスクリプトをコンテキスト隔離する必要があります

サンドボックス化されていないレンダラープロセスで contextIsolation フラグが有効になっていない場合、Node の ESM ローダーを介してファイルを動的に import() することはできません。

preload.mjs
// ❌ これらはコンテキスト隔離なしでは動作しません
const fs = await import('node:fs')
await import('./foo')

これは、レンダラープロセスでは Chromium の動的な ESM の import() 関数が通常は優先されて、コンテキスト隔離がないと Node.js が動的なインポート文を利用できるかを知る方法がないためです。 コンテキスト隔離を有効にすると、レンダラーでの隔離プリロードのコンテキストからの import() 文を Node.js のモジュールローダーへ転送できます。