Node.jsのモジュールタイプごとの読み込み方法
はじめに
CJSやESMといったモジュールタイプ、先人たちの記事を読み理解したつもりになるが、時間が経つと完全に忘れるということを繰り返している。package.jsonの最低限の設定と、その読み込み方法を簡易的に記載する。
これは検証で書いたサンプルコード
CJSからCJS, ESMを読み込む
パッケージ中の .js
は、パッケージのpackage.jsonに記載されている type
フィールドが commonjs
の場合はCJSとして扱われ、 type
フィールドが module
の場合はESMとして扱われる。type
フィールドのデフォルト値は commonjs
なので、 type
フィールドが指定されていなければCJSとして扱われる。
package.jsonの main
フィールドはパッケージのエントリーポイントで、パッケージを使用する側がimportまたはrequireする際に読み込まれるファイルとなる。main
フィールドのデフォルト値は index.js
のため、パッケージに index.js
しかない場合は指定不要(フォルダがパッケージのroot直下でない場合は指定は必要)。
{
"name": "package-esm",
"main": "index.js",
"type": "module"
}
{
"name": "package-cjs",
"main": "index.js",
"type": "commonjs"
}
パッケージを使用する側がCJSのとき、CJSなパッケージを読み込む際は require
関数を使用する。ESMなパッケージを読み込む際は import
を用いてダイナミックインポートする必要がある。
const packageCjs = require('package-cjs');
(async () => {
const packageEsm = await import('package-esm');
})();
ESMからCJS, ESMを読み込む
package-esm, package-cjsのpackage.jsonは前述したものと同じなため省略。
パッケージを読み込む側がESMのとき、パッケージがESMでもCJSでもimportで読み込める。
import packageEsm from 'package-esm';
import packageCjs from 'package-cjs';
CJS, ESMからDual Packageを読み込む
Dual Packageにする場合はESM、CJSに対応したコードをそれぞれ用意する必要がある(今回の検証では、ESMで書いたコードをesbuildでCJS形式に変換したものを使用した)。
package.jsonでは main
フィールドではなく、 exports
フィールドを用いてエントリーポイントを指定する。
また、exports
フィールドではConditional exportsとして、読み込み方法ごとにエントリーポイントを指定することができる。今回だと import
される場合はESMなファイルを、 require
される場合はCJSなファイルをエントリーポイントとしている。
{
"name": "package-dual",
"exports": {
".": {
"import": "./dist/esm/index.mjs",
"require": "./dist/cjs/index.js"
}
}
}
import packageDual from 'package-dual';
const packageDual = require('package-dual');
最初からわざわざDual Packageにする必要はなく、基本的にはESMで開発を進めていいと思っているが、だいたい依存している他のパッケージがESMに対応しておらず仕方なくDual Packageにする場合が多い気がする。
また、Dual Packageに関してはDual Packge Hazardという問題も発生するので気をつける必要がある。
おわりに
実際にコードを書く際は、TypeScriptやツールチェインなど様々なライブラリを組み合わせて使うことが多くなる。複数のライブラリが絡んだとき、そこに記載するモジュールの設定で混乱しがちだが、一つ一つを分解して考えると理解しやすくなる。
参考
Discussion