2024年にESMとCJSの両方に対応するnpmパッケージを作成する
2024年4月18日
0 分で読めます
ECMAScript Modules(ESM)とCommonJS(CJS)の両方に対応するJavaScriptパッケージの公開は、幅広いライブラリを統合したい開発者にとって重要なスキルです。
この記事では、ESMとCJSのサポートを維持するための実践的な方法とベストプラクティスを紹介します。両方に対応するライブラリで”type: module”宣言を避けることの影響を確認し、エントリーポイントを使い分けるためのpackage.jsonのmainフィールドとmoduleフィールドの使い方を見ていきます。
また、モジュール解決の制御に不可欠なexportsフィールドをpackage.jsonファイルで使う目的と方法についても解説します。さらに、TypeScriptプロジェクトについては、パッケージマニフェストのexportsとモジュールタイプを連携させ、互換性と型安全性の両方を確保する方法を紹介します。
以下のコードを順を追って試してみてください。すべての再現例を含むGitHubコードリポジトリpackage-json-exportsも用意しています。オープンソースのnpmプロキシプロジェクトVerdaccioを利用しています。
ESMとCJSに対応するライブラリでは「Type: Module」の定義を避ける
package.jsonファイルで”type”フィールドを省略すると、デフォルトでcommonjsが暗黙的に設定されることをご存じですか?たとえば、次のようになります。”type”: “commonjs”。
package.jsonファイルに”type”: “module”を追加すると、そのライブラリがESMプロジェクトのみを対象とすることを明示的に指定します。パッケージマニフェストには引き続きmainフィールドを定義する必要があり、ESM対応モジュールのエクスポートを反映するように更新されます。
具体例として、次のpackage.jsonの定義は「純粋な」ESMであるにもかかわらず、上流のESM利用者では動作しません。
moduleの定義はESMモジュールであり、typeによってこのライブラリがESM利用者を対象としていることも明確です。しかし、package.jsonファイルにmainフィールドがありません。そのため、Node.jsはパッケージを見つけられないというエラーを出して例外をスローします。たとえば、次のようになります。
まとめると、npmパッケージのパッケージマニフェストでは”type”: “module”宣言を使わないようにしましょう。
package.jsonファイルのmainフィールドとmoduleフィールドの使い方を見て、これらがより適切な指定方法である理由を解説します。
mainフィールドとmoduleフィールドによるESMとCJSの互換性
ESMが登場する前は、package.jsonファイルのmainフィールドで、Node.jsランタイムにパッケージのエントリーポイントを指定していました。通常、開発者はルートディレクトリにindex.jsやapp.jsファイルを置き、mainフィールドでそのファイルを指定していました。たとえば、”main”: “index.js”のようにします。
package.jsonファイルでmainフィールドを省略すると、Node.jsランタイムはパッケージのルートディレクトリにあるserver.jsをファイル規則に基づいてエントリーポイントとして解決します。
ESMとCJSの両方に対応するnpmパッケージを定義するには、mainでCJSエクスポートを指定し、moduleでESMエクスポートを指定する方法が使えます。
ライブラリ:
上流の利用側にはCJSプロジェクトとESMプロジェクトの両方があります。CJSプロジェクトはsrc/index.cjsファイルを、ESMプロジェクトはsrc/index.mjsファイルを利用します。どちらの利用側もmath-add依存関係について特別な指定をする必要はなく、そのまま動作します。
package.jsonのexportsフィールドを理解する
package.jsonファイルでexportsフィールドを使うと、npmパッケージからエクスポートする要素と、その利用方法をさらに細かく制御できます。
たとえば、Node.jsランタイムがrequire(‘math-add’)でnpmパッケージを読み込もうとした場合にエントリーファイルのフルパスを指定し、import .. from ‘math-add’)で読み込もうとした場合には、まったく別のファイルをエントリーとして指定できます。
以下は、説明したデュアルモードのCJS・ESMパッケージのコード例です。
TypeScriptプロジェクトでのpackage.jsonのexportsとモジュールタイプ
TypeScriptでパッケージのESMコードを記述し、CJSとの後方互換性も維持したい場合は、typesを宣言し、CJS部分のTypeScriptコンパイルとトランスパイルも設定する必要があります。
TypeScriptのコンパイルとバンドルには、tsupをおすすめします。次にbuildスクリプトのステージを用意します。CIジョブやnpmパッケージの手動公開を実行する前に、必ずビルドを実行してください。
完全な例を以下に示します。
変更を監視するNode.jsランタイムの新しいサポートも、コマンドラインフラグ--watch srcを使って利用していることにお気づきでしょう。以前はnodemonで実現していました。優れたパッケージですが、依存関係は少ないほうが望ましいです。
次のステップ:2024年の最新npmパッケージの公開と構成
この記事では、JavaScript開発者向けに、プロジェクトでモジュール形式を効果的に扱うための、実践しやすく具体的なポイントを簡潔に紹介しました。
npmパッケージの公開に関するベストプラクティスに従い、TypeScriptの設定、テスト、CI、セキュリティなどをさらに深く考慮した最新のnpmパッケージを作成することも大切です。
オープンソースの依存関係を安全に保つ
Snykは、脆弱なオープンソースの依存関係とその推移的依存関係を、ワンクリックで修正するPRを提供します。


