デュアルパッケージNPMモジュールの構築と公開
Min-jun Kim
Dev Intern · Leapcell

はじめに
絶えず進化するJavaScriptエコシステムにおいて、再利用可能なコードの共有は効率的な開発の基盤です。NPMパッケージはこれを支える要であり、開発者はソリューションをモジュール化し、より広範なコミュニティに貢献することができます。しかし、長年続いているCommonJS (CJS) 標準に加えてECMAScript Modules (ESM) が台頭する中で、両方の環境にシームレスに統合されるパッケージをオーサリングすることは、重要でありながらも時に困難なタスクとなっています。どちらかの標準を無視すると、パッケージの採用が制限されたり、ユーザーが複雑な回避策を強いられたりする可能性があります。この記事では、そのプロセスを解明し、ESMとCJSの両方をスムーズにサポートする独自のNPMパッケージの作成、テスト、公開方法に関する包括的なガイドを提供します。
主要概念の理解
実装に進む前に、関連する主要な概念についての共通認識を確立しましょう。
- CommonJS (CJS): Node.jsによって普及したこのモジュールシステムは、モジュールのインポートに
require()
を、エクスポートにmodule.exports
を使用します。これは同期的なもので、長年サーバーサイドJavaScriptのデフォルトでした。// CJS import const myModule = require('./myModule.js'); // CJS export module.exports = { myFunction: () => console.log('Hello from CJS!') };
- ECMAScript Modules (ESM): ES2015で導入されたJavaScriptの公式モジュール標準です。
import
とexport
ステートメントを使用し、これらは本質的に非同期であり、ブラウザとNode.js環境の両方で設計されています。// ESM import import { myFunction } from './myModule.js'; // ESM export export const myFunction = () => console.log('Hello from ESM!');
- デュアルパッケージハザード: パッケージがCJSとESMの両方のバージョンを提供しようとしたときに発生する可能性のある問題を指します。異なる環境が異なるモジュールタイプに解決されたり、状態が重複したりすると、問題が発生する可能性があります。
- 条件付きエクスポート:
package.json
の強力な機能で、環境(例:ESMの場合はimport
、CJSの場合はrequire
、Webの場合はbrowser
)に基づいて異なるエントリーポイントを定義できます。これはデュアルパッケージハザードを解決する鍵となります。// package.json スニペット:条件付きエクスポート "exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" } }
package.json
のtype
フィールド: このフィールド("type": "module"
または"type": "commonjs"
)は、パッケージまたはスコープ内のすべての.js
ファイルに対するデフォルトのモジュールシステムを定義します。Node.jsがファイルを解釈する方法において重要な役割を果たします。- トランスパイル: ある言語またはバージョンのソースコードを別の言語またはバージョンに変換するプロセスです。JavaScriptでは、これはしばしば、BabelやTypeScriptなどのツールを使用して、より広範な互換性のために新しい構文(ESMの
import/export
など)を古い構文(CJSのrequire/module.exports
など)に変換することを意味します。
デュアルパッケージモジュールの構築
CJSとESMの両方でシームレスに動作することを保証する、ユーザーに挨拶するシンプルなユーティリティパッケージを作成する手順を追ってみましょう。
1. プロジェクトのセットアップと初期化
まず、パッケージ用の新しいディレクトリを作成し、NPMプロジェクトを初期化します。
mkdir my-greeting-package cd my-greeting-package npm init -y
2. ソースコード
私たちのパッケージには、ユーザーに挨拶を返す単一の関数があります。これは最新のESM構文を使用して記述します。
src/index.js
:
// src/index.js export function greet(name = 'World') { return `Hello, ${name}!`; }
3. ビルド構成とトランスパイル
CJSとESMの両方をサポートするには、ソースコードをトランスパイルする必要があります。Rollup.jsを使用します。これは非常に設定可能で、ライブラリバンドリングに優れているためです。
npm install --save-dev rollup @rollup/plugin-terser @rollup/plugin-node-resolve
rollup.config.js
ファイルを作成します。
// rollup.config.js import { terser } from '@rollup/plugin-terser'; import { nodeResolve } from '@rollup/plugin-node-resolve'; export default [ // CJSビルド { input: 'src/index.js', output: { file: 'dist/cjs/index.js', format: 'cjs', sourcemap: true, exports: 'named', // named exportがCJSで正しく処理されることを確認します }, plugins: [nodeResolve(), terser()], }, // ESMビルド { input: 'src/index.js', output: { file: 'dist/esm/index.js', format: 'esm', sourcemap: true, }, plugins: [nodeResolve(), terser()], }, ];
package.json
にビルドスクリプトを追加します。
// package.json スニペット "scripts": { "build": "rollup -c", "test": "node test/test.js" // 後で追加します }, "main": "dist/cjs/index.js", // 古いNode.jsやツール向けのフォールバック "module": "dist/esm/index.js", // Webpack/Rollupのようなバンドラーのヒント "type": "commonjs", // パッケージのデフォルトタイプ
ビルドを実行します。
npm run build
これにより、dist/cjs/index.js
とdist/esm/index.js
が作成されます。
4. デュアルパッケージサポートのためのpackage.json
の構成
これは最も重要なステップです。条件付きエクスポートを使用するようにpackage.json
を変更し、環境が正しいモジュールタイプを選択するようにしました。
// package.json { "name": "my-greeting-package", "version": "1.0.0", "description": "A simple package that greets the user.", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "type": "commonjs", "exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js", "default": "./dist/cjs/index.js" }, "./package.json": "./package.json" }, "files": [ "dist" ], "scripts": { "build": "rollup -c", "test": "node test/test.js" }, "keywords": [ "greeting", "esm", "cjs" ], "author": "Your Name", "license": "MIT", "devDependencies": { "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", "rollup": "^4.12.0" } }
main
: CJS環境(レガシーフォールバック)のエントリーポイントを指定します。module
: ESM対応バンドラー(例:Webpack、Rollup)のエントリーポイントを指定します。type: "commonjs"
: これはNode.jsに、このパッケージ内の.js
ファイルがデフォルトでCJSであることを伝えます。ESM用の.mjs
ファイルがあった場合、これは不要になるか、"module"
に設定してCJSファイルに.cjs
を使用することもできます。現在のセットアップでは、RollupがESM構文を出力し、exports.import
条件が優先されるため、dist/esm/index.js
は引き続きESMとして機能します。exports
: ここが魔法の起こる場所です。.
: パッケージのプライマリーエントリーポイントを定義します。import
:import
が使用されたときにESMバージョンを指します。require
:require
が使用されたときにCJSバージョンを指します。default
:import
またはrequire
条件を認識しない環境、または将来の保証のためのフォールバックです。"./package.json": "./package.json"
: 他のパッケージがNPMに公開する際に、package.json
に直接アクセスできるようにすることが重要です。
files
: NPMに公開されるファイルを指定します。これにより、パッケージを軽量に保つことができます。
5. パッケージのテスト
堅牢なテストは不可欠です。CJS用とESM用の2つのテストファイルを作成します。
test/cjs-test.js
:
// test/cjs-test.js const { greet } = require('../'); // パッケージ自体をrequireしていることに注意 if (typeof greet === 'function') { console.log('CJS Test: greet is a function'); const result1 = greet(); console.log('CJS Test:', result1); // 期待値: Hello, World! const result2 = greet('Alice'); console.log('CJS Test:', result2); // 期待値: Hello, Alice! if (result1 === 'Hello, World!' && result2 === 'Hello, Alice!') { console.log('CJS Test PASSED'); } else { console.error('CJS Test FAILED'); process.exit(1); } } else { console.error('CJS Test FAILED: greet is not a function'); process.exit(1); }
test/esm-test.mjs
:
// test/esm-test.mjs import { greet } from '../'; // パッケージ自体をimportしていることに注意 async function runEsmTest() { if (typeof greet === 'function') { console.log('ESM Test: greet is a function'); const result1 = greet(); console.log('ESM Test:', result1); // 期待値: Hello, World! const result2 = greet('Bob'); console.log('ESM Test:', result2); // 期待値: Hello, Bob! if (result1 === 'Hello, World!' && result2 === 'Hello, Bob!') { console.log('ESM Test PASSED'); } else { console.error('ESM Test FAILED'); process.exit(1); } } else { console.error('ESM Test FAILED: greet is not a function'); process.exit(1); } } runEsmTest().catch(err => { console.error('ESM Test encountered an error:', err); process.exit(1); });
package.json
のスクリプトを両方のテストを実行するように更新します。
// package.json スニペット "scripts": { "build": "rollup -c", "test": "node test/cjs-test.js && node test/esm-test.mjs" },
これで、テストを実行します。
npm run test
両方のテストがパスし、パッケージがCJSとESMの両方の環境に正しくエクスポートされていることを示すはずです。
6. NPMへの公開
公開する前に、以下を確認してください。
- NPMアカウントを持っていること。
- ターミナルからNPMにログインしていること (
npm login
)。 package.json
に一意のname
と適切なversion
があること。files
配列が公開されるものを正しくリストしていること(例:["dist", "README.md", "LICENSE"]
)。
最後に、公開するには:
npm publish
パッケージを更新する必要がある場合は、package.json
でバージョンをインクリメントしてから、再度npm publish
を実行してください。
結論
ESMとCJSの両方をサポートするNPMパッケージを作成することは、現代のJavaScript開発者にとって重要なスキルです。Rollupのようなツールをトランスパイルに使用し、条件付きexports
でpackage.json
を注意深く構成することにより、堅牢で普遍的に互換性のあるモジュールを提供できます。このアプローチはユーザーの摩擦を最小限に抑え、貴重なコードのリーチを最大化します。これらのプラクティスを採用することで、パッケージは多様なJavaScriptランドスケープ全体で関連性がありアクセス可能であり続けます。