🐙

Node.jsのモジュールタイプごとの読み込み方法

2024/05/26に公開

はじめに

CJSやESMといったモジュールタイプ、先人たちの記事を読み理解したつもりになるが、時間が経つと完全に忘れるということを繰り返している。package.jsonの最低限の設定と、その読み込み方法を簡易的に記載する。

これは検証で書いたサンプルコード
https://github.com/kkznch/sample-dual-package

CJSからCJS, ESMを読み込む

パッケージ中の .js は、パッケージのpackage.jsonに記載されている type フィールドが commonjs の場合はCJSとして扱われ、 type フィールドが module の場合はESMとして扱われる。type フィールドのデフォルト値は commonjs なので、 type フィールドが指定されていなければCJSとして扱われる。
https://nodejs.org/api/packages.html#type

package.jsonの main フィールドはパッケージのエントリーポイントで、パッケージを使用する側がimportまたはrequireする際に読み込まれるファイルとなる。main フィールドのデフォルト値は index.js のため、パッケージに index.js しかない場合は指定不要(フォルダがパッケージのroot直下でない場合は指定は必要)。
https://nodejs.org/api/packages.html#main

package-esm/package.json
{
  "name": "package-esm",
  "main": "index.js",
  "type": "module"
}
package-cjs/package.json
{
  "name": "package-cjs",
  "main": "index.js",
  "type": "commonjs"
}

パッケージを使用する側がCJSのとき、CJSなパッケージを読み込む際は require 関数を使用する。ESMなパッケージを読み込む際は import を用いてダイナミックインポートする必要がある。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/import

app-cjs/index.js
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で読み込める。

app-esm/index.js
import packageEsm from 'package-esm';
import packageCjs from 'package-cjs';

CJS, ESMからDual Packageを読み込む

Dual Packageにする場合はESM、CJSに対応したコードをそれぞれ用意する必要がある(今回の検証では、ESMで書いたコードをesbuildでCJS形式に変換したものを使用した)。
https://esbuild.github.io/

package.jsonでは main フィールドではなく、 exports フィールドを用いてエントリーポイントを指定する。
https://nodejs.org/api/packages.html#exports

また、exports フィールドではConditional exportsとして、読み込み方法ごとにエントリーポイントを指定することができる。今回だと import される場合はESMなファイルを、 require される場合はCJSなファイルをエントリーポイントとしている。
https://nodejs.org/api/packages.html#conditional-exports

package-dual/package.json
{
  "name": "package-dual",
  "exports": {
    ".": {
      "import": "./dist/esm/index.mjs",
      "require": "./dist/cjs/index.js"
    }
  }
}
app-esm/index.js
import packageDual from 'package-dual';
app-cjs/index.js
const packageDual = require('package-dual');

最初からわざわざDual Packageにする必要はなく、基本的にはESMで開発を進めていいと思っているが、だいたい依存している他のパッケージがESMに対応しておらず仕方なくDual Packageにする場合が多い気がする。
また、Dual Packageに関してはDual Packge Hazardという問題も発生するので気をつける必要がある。
https://zenn.dev/hankei6km/articles/test-dual-package-hazard

おわりに

実際にコードを書く際は、TypeScriptやツールチェインなど様々なライブラリを組み合わせて使うことが多くなる。複数のライブラリが絡んだとき、そこに記載するモジュールの設定で混乱しがちだが、一つ一つを分解して考えると理解しやすくなる。

参考

https://docs.npmjs.com/cli/v10/configuring-npm/package-json
https://de-milestones.com/what-is-cjs-umd-esm/#toc3
https://numb86-tech.hatenablog.com/entry/2020/08/07/091142
https://zenn.dev/makotot/articles/5edb504ef7d2e6
https://blog.cybozu.io/entry/2020/10/06/170000#CJS-を直接-import-する
https://tsmx.net/commonjs-vs-esm-ecmascript-cheat-sheet/

株式会社モニクル

Discussion