モジュール: パッケージ#

はじめに#

パッケージは、`package.json` ファイルで記述されたフォルダーツリーです。パッケージは、`package.json` ファイルを含むフォルダーと、次の`package.json` ファイルを含むフォルダー、または`node_modules` という名前のフォルダーに到達するまでのすべてのサブフォルダーで構成されます。

このページでは、`package.json` ファイルを作成するパッケージ作成者向けのガイダンスと、Node.js で定義されている`package.json` フィールドのリファレンスを提供します。

モジュールシステムの決定#

はじめに#

Node.js は、初期入力として`node` に渡された場合、または`import` 文や`import()` 式によって参照された場合、以下をES モジュール として扱います。

  • ` .mjs` 拡張子のファイル。

  • 最も近い親`package.json` ファイルに、` "module"` の値を持つ最上位レベルの` "type"` フィールドが含まれている場合の`.js` 拡張子のファイル。

  • ` --eval` の引数として渡された文字列、または` --input-type=module` フラグを使用して` STDIN` を介して`node` にパイプされた文字列。

  • ` --experimental-detect-module` を使用する場合、`import` や`export` 文、`import.meta` など、ES モジュール としてのみ正常に解析される構文を含むコードで、解釈方法の明示的なマーカーがない場合。明示的なマーカーは、`.mjs` または`.cjs` 拡張子、`package.json` の`"type"` フィールド(`"module"` または`"commonjs"` の値を持つ)、または` --input-type` や` --experimental-default-type` フラグです。動的な`import()` 式は、CommonJS モジュールまたは ES モジュールでサポートされており、ファイルが ES モジュールとして扱われる原因にはなりません。

Node.js は、初期入力として`node` に渡された場合、または`import` 文や`import()` 式によって参照された場合、以下をCommonJS として扱います。

  • ` .cjs` 拡張子のファイル。

  • 最も近い親`package.json` ファイルに、`"commonjs"` の値を持つ最上位レベルのフィールド` "type"` が含まれている場合の`.js` 拡張子のファイル。

  • ` --eval` や` --print` の引数として渡された文字列、または` --input-type=commonjs` フラグを使用して` STDIN` を介して`node` にパイプされた文字列。

これらの明示的なケース以外にも、` --experimental-default-type` フラグの値に基づいて、Node.js が一方のモジュールシステムをデフォルトで選択するその他のケースがあります。

  • `.js` で終わるファイル、または拡張子がないファイルで、同じフォルダーまたは親フォルダーに`package.json` ファイルが存在しない場合。

  • `.js` で終わるファイル、または拡張子がないファイルで、最も近い親`package.json` フィールドに`"type"` フィールドがない場合(フォルダーが`node_modules` フォルダー内にある場合を除く)。(`node_modules` の下のパッケージスコープは、後方互換性のために、`package.json` ファイルに`"type"` フィールドがない場合、` --experimental-default-type` に関係なく、常に CommonJS として扱われます。)

  • ` --input-type` が指定されていない場合、` --eval` の引数として渡された文字列、または` STDIN` を介して`node` にパイプされた文字列。

このフラグは現在`"commonjs"` をデフォルトとしていますが、将来`"module"` をデフォルトにする可能性があります。このため、できる限り明示的にすることが最善です。特に、パッケージ作成者は、すべてのソースが CommonJS であるパッケージでも、常に`package.json` ファイルに` "type"` フィールドを含める必要があります。パッケージの`type` を明示的にすることで、Node.js のデフォルトのタイプが変更された場合にパッケージが将来対応できるようになり、ビルドツールやローダーがパッケージ内のファイルをどのように解釈すべきかを決定しやすくなります。

モジュールローダー#

Node.js には、指定子の解決とモジュールの読み込みのための 2 つのシステムがあります。

CommonJS モジュールローダーがあります。

  • 完全に同期しています。
  • `require()` 呼び出しの処理を担当します。
  • モンキーパッチ可能です。
  • フォルダーをモジュールとして サポートしています。
  • 指定子を解決する場合、正確な一致が見つからないと、拡張子(`.js`、`.json`、最後に`.node`)を追加してから、フォルダーをモジュールとして 解決しようとします。
  • `.json` を JSON テキストファイルとして扱います。
  • `.node` ファイルは、`process.dlopen()` で読み込まれたコンパイル済みアドオンモジュールとして解釈されます。
  • `.json` や`.node` 拡張子がないすべてのファイルを JavaScript テキストファイルとして扱います。
  • ECMAScript モジュールの読み込みには使用できません(ただし、CommonJS モジュールから ECMASCript モジュールを読み込む ことは可能です)。ECMAScript モジュールではない JavaScript テキストファイルを読み込む場合、CommonJS モジュールとして読み込みます。

ECMAScript モジュールローダーがあります。

  • 非同期です。
  • `import` 文と`import()` 式の処理を担当します。
  • モンキーパッチできません。ローダーフック を使用してカスタマイズできます。
  • フォルダーをモジュールとしてサポートしていません。ディレクトリインデックス(例:`'./startup/index.js'`)は完全に指定する必要があります。
  • 拡張子の検索を行いません。指定子が相対または絶対ファイルURL の場合、ファイル拡張子を指定する必要があります。
  • JSON モジュールを読み込むことができますが、インポートアサーションが必要です。
  • JavaScript テキストファイルには、`.js`、`.mjs`、`.cjs` 拡張子のみを受け入れます。
  • JavaScript CommonJS モジュールを読み込むことができます。このようなモジュールは`cjs-module-lexer` を通して渡され、静的解析で特定できる場合は、名前付きエクスポートを識別しようとします。インポートされた CommonJS モジュールの URL は絶対パスに変換され、CommonJS モジュールローダーを介して読み込まれます。

`package.json` とファイル拡張子#

パッケージ内では、`package.json`` "type"` フィールドによって、Node.js が`.js` ファイルをどのように解釈するかが定義されます。`package.json` ファイルに`"type"` フィールドがない場合、`.js` ファイルはCommonJS として扱われます。

`package.json` の`"type"` の値が`"module"` の場合、Node.js はそのパッケージ内の`.js` ファイルをES モジュール構文を使用するものとして解釈します。

`"type"` フィールドは、初期エントリポイント(`node my-app.js`)だけでなく、`import` 文や`import()` 式によって参照されるファイルにも適用されます。

// my-app.js, treated as an ES module because there is a package.json
// file in the same folder with "type": "module".

import './startup/init.js';
// Loaded as ES module since ./startup contains no package.json file,
// and therefore inherits the "type" value from one level up.

import 'commonjs-package';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs".

import './node_modules/commonjs-package/index.js';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs". 

` .mjs` で終わるファイルは、最も近い親`package.json` に関係なく、常にES モジュールとして読み込まれます。

` .cjs` で終わるファイルは、最も近い親`package.json` に関係なく、常にCommonJSとして読み込まれます。

import './legacy-file.cjs';
// Loaded as CommonJS since .cjs is always loaded as CommonJS.

import 'commonjs-package/src/index.mjs';
// Loaded as ES module since .mjs is always loaded as ES module. 

` .mjs` と`.cjs` の拡張子を使用して、同じパッケージ内でタイプを混在させることができます。

  • `"type": "module"` パッケージ内では、`.cjs` 拡張子を付けて名前を付けることで、特定のファイルをCommonJSとして解釈するように Node.js に指示できます(`.js` と`.mjs` の両方のファイルは`"module"` パッケージ内では ES モジュールとして扱われるため)。

  • `"type": "commonjs"` パッケージ内では、`.mjs` 拡張子を付けて名前を付けることで、特定のファイルをES モジュールとして解釈するように Node.js に指示できます(`.js` と`.cjs` の両方のファイルは`"commonjs"` パッケージ内では CommonJS として扱われるため)。

` --input-type` フラグ#

` --eval`(または` -e`)の引数として渡された文字列、または` STDIN` を介して`node` にパイプされた文字列は、` --input-type=module` フラグが設定されている場合、ES モジュールとして扱われます。

node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"

echo "import { sep } from 'node:path'; console.log(sep);" | node --input-type=module 

完全性を期すために、文字列入力を CommonJS として明示的に実行するための` --input-type=commonjs` もあります。これは、` --input-type` が指定されていない場合のデフォルトの動作です。

パッケージマネージャーの決定#

安定性: 1 - 試験段階

Node.jsプロジェクトは公開後、すべてのパッケージマネージャーでインストール可能であることが期待されますが、開発チームは特定のパッケージマネージャーを使用することが求められることがよくあります。このプロセスを容易にするため、Node.jsには、Node.jsがインストールされていれば、すべてのパッケージマネージャーを透過的に利用できるようにすることを目指したCorepackというツールが付属しています。

デフォルトでは、Corepackは特定のパッケージマネージャーを強制せず、各Node.jsリリースに関連付けられた一般的な「最終的に良好だった」バージョンを使用しますが、プロジェクトのpackage.jsonファイルに"packageManager"フィールドを設定することで、このエクスペリエンスを向上させることができます。

パッケージのエントリポイント#

パッケージのpackage.jsonファイルでは、2つのフィールドでパッケージのエントリポイントを定義できます。"main""exports"です。両方のフィールドは、ESモジュールとCommonJSモジュールの両方のエントリポイントに適用されます。

"main"フィールドはすべてのバージョンのNode.jsでサポートされていますが、その機能は限定的です。パッケージのメインエントリポイントのみを定義します。

"exports""main"の現代的な代替手段であり、複数のエントリポイントの定義、環境間の条件付きエントリ解決のサポート、そして"exports"で定義されたもの以外のエントリポイントを防止することを可能にします。このカプセル化により、モジュール作成者はパッケージのパブリックインターフェースを明確に定義できます。

現在サポートされているNode.jsのバージョンをターゲットとする新しいパッケージの場合は、"exports"フィールドが推奨されます。Node.js 10以下のバージョンをサポートするパッケージの場合は、"main"フィールドが必要です。 "exports""main"の両方が定義されている場合、サポートされているNode.jsのバージョンでは、"exports"フィールドが"main"フィールドよりも優先されます。

条件付きエクスポートは、"exports"内で使用して、環境ごとに異なるパッケージエントリポイントを定義できます。パッケージがrequire経由で参照されるか、import経由で参照されるかも含まれます。単一のパッケージでCommonJSとESモジュールの両方をサポートする方法の詳細については、デュアルCommonJS/ESモジュールパッケージのセクションを参照してください。

"exports"フィールドを導入する既存のパッケージでは、package.json(例:require('your-package/package.json'))を含む、定義されていないエントリポイントをパッケージのコンシューマーが使用できなくなります。これは破壊的変更になる可能性が高いです。

"exports"の導入を破壊的変更にしないためには、以前サポートされていたすべてのエントリポイントをエクスポートする必要があります。パッケージのパブリックAPIが明確に定義されるように、エントリポイントを明示的に指定することをお勧めします。たとえば、以前はmainlibfeature、およびpackage.jsonをエクスポートしていたプロジェクトでは、次のpackage.exportsを使用できます。

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/index": "./lib/index.js",
    "./lib/index.js": "./lib/index.js",
    "./feature": "./feature/index.js",
    "./feature/index": "./feature/index.js",
    "./feature/index.js": "./feature/index.js",
    "./package.json": "./package.json"
  }
} 

あるいは、プロジェクトは拡張子付きと拡張子なしの両方でサブパスを含むフォルダ全体をエクスポートすることを選択できます。

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/*": "./lib/*.js",
    "./lib/*.js": "./lib/*.js",
    "./feature": "./feature/index.js",
    "./feature/*": "./feature/*.js",
    "./feature/*.js": "./feature/*.js",
    "./package.json": "./package.json"
  }
} 

上記のことで、マイナーバージョンのパッケージに対する下位互換性が確保されるため、パッケージの将来のメジャー変更では、エクスポートを公開された特定の機能エクスポートのみに適切に制限できます。

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./feature/*.js": "./feature/*.js",
    "./feature/internal/*": null
  }
} 

メインエントリポイントのエクスポート#

新しいパッケージを作成する場合は、"exports"フィールドを使用することをお勧めします。

{
  "exports": "./index.js"
} 

"exports"フィールドが定義されている場合、パッケージのすべてのサブパスはカプセル化され、インポーターは使用できなくなります。たとえば、require('pkg/subpath.js')ERR_PACKAGE_PATH_NOT_EXPORTEDエラーをスローします。

このエクスポートのカプセル化により、ツールのパッケージインターフェースと、パッケージのsemverアップグレードの処理に関する信頼性の高い保証が提供されます。パッケージの絶対サブパスの直接的なrequire(例:require('/path/to/node_modules/pkg/subpath.js'))は依然としてsubpath.jsをロードするため、強力なカプセル化ではありません。

現在サポートされているすべてのバージョンのNode.jsと最新のビルドツールは、"exports"フィールドをサポートしています。古いバージョンのNode.jsまたは関連するビルドツールを使用しているプロジェクトでは、同じモジュールを指す"main"フィールドを"exports"とともに含めることで、互換性を達成できます。

{
  "main": "./index.js",
  "exports": "./index.js"
} 

サブパスのエクスポート#

"exports"フィールドを使用する場合、メインエントリポイントを"."サブパスとして扱うことで、メインエントリポイントとともにカスタムサブパスを定義できます。

{
  "exports": {
    ".": "./index.js",
    "./submodule.js": "./src/submodule.js"
  }
} 

これで、"exports"で定義されたサブパスのみをコンシューマーがインポートできます。

import submodule from 'es-module-package/submodule.js';
// Loads ./node_modules/es-module-package/src/submodule.js 

他のサブパスはエラーになります。

import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED 
サブパスでの拡張子#

パッケージ作成者は、拡張子付き(import 'pkg/subpath.js')または拡張子なし(import 'pkg/subpath')のサブパスをエクスポートに提供する必要があります。これにより、エクスポートされたモジュールごとにサブパスが1つだけになり、すべての依存関係が同じ一貫性のある指定子をインポートするため、コンシューマーにとってパッケージコントラクトが明確になり、パッケージサブパスの補完が簡素化されます。

従来、パッケージは拡張子なしのスタイルを使用することが多く、これは可読性とパッケージ内のファイルの実際のパスを隠すという利点がありました。

import mapsがブラウザやその他のJavaScriptランタイムでのパッケージ解決の標準を提供するようになったため、拡張子なしのスタイルを使用すると、import mapの定義が肥大化する可能性があります。明示的なファイル拡張子を使用すると、可能な限り複数のサブパスをマップするpackagesフォルダマッピングをimport mapで使用できるため、この問題を回避できます。これは、相対インポート指定子と絶対インポート指定子で完全な指定子パスを使用するという要件も反映しています。

エクスポートの糖衣構文#

"."エクスポートが唯一のエクスポートである場合、"exports"フィールドは、このケースに対する糖衣構文を提供し、直接"exports"フィールドの値になります。

{
  "exports": {
    ".": "./index.js"
  }
} 

と記述できます。

{
  "exports": "./index.js"
} 

サブパスのインポート#

"exports"フィールドに加えて、パッケージの"imports"フィールドを使用して、パッケージ自体からのインポート指定子にのみ適用されるプライベートマッピングを作成できます。

"imports"フィールドのエントリは、常に#で始まる必要があります。これにより、外部パッケージ指定子と区別されます。

たとえば、importsフィールドを使用して、内部モジュールの条件付きエクスポートの利点を活用できます。

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
} 

ここで、import '#dep'は、外部パッケージdep-node-native(そのエクスポートを含む)の解決を取得せず、代わりに他の環境ではパッケージを基準としたローカルファイル./dep-polyfill.jsを取得します。

"exports"フィールドとは異なり、"imports"フィールドは外部パッケージへのマッピングを許可します。

importsフィールドの解決ルールは、それ以外の場合、exportsフィールドと同様です。

サブパスパターン#

エクスポートまたはインポートの数が少ないパッケージの場合、各エクスポートサブパスエントリを明示的にリストすることをお勧めします。しかし、サブパスが多いパッケージの場合、これによりpackage.jsonが肥大化し、メンテナンスの問題が発生する可能性があります。

このようなユースケースでは、サブパスのエクスポートパターンを使用できます。

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js"
  },
  "imports": {
    "#internal/*.js": "./src/internal/*.js"
  }
} 

*マップは、文字列置換構文のみであるため、ネストされたサブパスを公開します。

右辺の*のすべてのインスタンスは、この値に置き換えられます。/セパレータが含まれている場合も同様です。

import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js

import featureY from 'es-module-package/features/y/y.js';
// Loads ./node_modules/es-module-package/src/features/y/y.js

import internalZ from '#internal/z.js';
// Loads ./node_modules/es-module-package/src/internal/z.js 

これは、ファイル拡張子に関する特別な処理を行わない直接的な静的マッチングと置換です。マッピングの両側に"*.js"を含めることで、公開されたパッケージエクスポートをJSファイルのみに制限します。

エクスポートパターンを使用しても、エクスポートが静的に列挙可能であるというプロパティは維持されます。これは、パッケージの個々のエクスポートを、右辺のターゲットパターンをパッケージ内のファイルのリストに対する** globとして扱うことで判断できるためです。node_modulesパスはエクスポートターゲットでは禁止されているため、この展開はパッケージ自体のファイルのみに依存します。

プライベートサブフォルダをパターンから除外するには、nullターゲットを使用できます。

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js",
    "./features/private-internal/*": null
  }
} 
import featureInternal from 'es-module-package/features/private-internal/m.js';
// Throws: ERR_PACKAGE_PATH_NOT_EXPORTED

import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js 

条件付きエクスポート#

条件付きエクスポートにより、特定の条件に応じて異なるパスにマップする方法が提供されます。CommonJSとESモジュールのインポートの両方でサポートされています。

たとえば、require()importで異なるESモジュールエクスポートを提供するパッケージは、次のように記述できます。

// package.json
{
  "exports": {
    "import": "./index-module.js",
    "require": "./index-require.cjs"
  },
  "type": "module"
} 

Node.jsは、条件を定義する必要があるため、最も具体的なものから最も具体的でないものまで、次の条件を実装します。

  • "node-addons" - "node"と似ており、Node.js環境に一致します。この条件は、ネイティブC++アドオンを使用するエントリポイントを提供する場合、より汎用的でネイティブアドオンに依存しないエントリポイントとは対照的に使用できます。この条件は、--no-addonsフラグで無効にできます。
  • "node" - 任意のNode.js環境に一致します。CommonJSまたはESモジュールファイルにすることができます。ほとんどの場合、Node.jsプラットフォームを明示的に指定する必要はありません。
  • "import" - パッケージがimportまたはimport()、またはECMAScriptモジュールローダーによるトップレベルのインポートまたは解決操作によってロードされた場合に一致します。ターゲットファイルのモジュール形式に関係なく適用されます。常に"require"と相互に排他的です。
  • "require" - パッケージがrequire()によってロードされた場合に一致します。参照されるファイルはrequire()でロード可能である必要がありますが、条件はターゲットファイルのモジュール形式に関係なく一致します。期待される形式には、CommonJS、JSON、ネイティブアドオンが含まれますが、require()はサポートしていないため、ESモジュールは含まれません。常に"import"と相互に排他的です。
  • "default" - 常に一致する一般的なフォールバック。CommonJSまたはESモジュールファイルにすることができます。この条件は常に最後に来る必要があります。

"exports" オブジェクト内では、キーの順序が重要です。条件の一致において、先に記述されたエントリの方が優先順位が高くなり、後続のエントリよりも優先されます。 *一般的なルールとして、条件はオブジェクトの順序で最も具体的なものから最も抽象的なものへと記述する必要があります。*

"import" および "require" 条件を使用すると、いくつかの危険性があります。デュアルCommonJS/ESモジュールパッケージのセクションで詳しく説明します。

"node-addons" 条件は、ネイティブC++アドオンを使用するエントリポイントを提供するために使用できます。ただし、この条件は--no-addons フラグで無効にすることができます。"node-addons" を使用する場合、"default" を、より普遍的なエントリポイント(例:ネイティブアドオンの代わりにWebAssemblyを使用)を提供する拡張機能として扱うことをお勧めします。

条件付きエクスポートは、エクスポートのサブパスにも拡張できます。例:

{
  "exports": {
    ".": "./index.js",
    "./feature.js": {
      "node": "./feature-node.js",
      "default": "./feature.js"
    }
  }
} 

require('pkg/feature.js')import 'pkg/feature.js' で、Node.jsと他のJS環境間で異なる実装を提供できるパッケージを定義します。

環境ブランチを使用する場合は、可能な限り常に"default"条件を含めてください。"default"条件を提供することで、未知のJS環境でもこの普遍的な実装を使用できるようになります。これにより、これらのJS環境が条件付きエクスポートを持つパッケージをサポートするために、既存の環境を装う必要がなくなります。このため、"node""default"条件ブランチを使用する方が、"node""browser"条件ブランチを使用するよりも通常は好ましいです。

ネストされた条件#

直接マッピングに加えて、Node.jsはネストされた条件オブジェクトもサポートしています。

例えば、Node.jsでは使用できるがブラウザでは使用できないデュアルモードのエントリポイントのみを持つパッケージを定義するには

{
  "exports": {
    "node": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    },
    "default": "./feature.mjs"
  }
} 

フラットな条件と同様に、条件は順に照合され続けます。ネストされた条件にマッピングがない場合、親条件の残りの条件の確認を続けます。このように、ネストされた条件は、ネストされたJavaScriptのif文と同様に動作します。

ユーザー条件の解決#

Node.jsを実行する際に、--conditionsフラグを使用してカスタムユーザー条件を追加できます。

node --conditions=development index.js 

これにより、パッケージのインポートとエクスポートで"development"条件が解決され、既存の"node""node-addons""default""import""require"条件が適切に解決されます。

繰り返しフラグを使用して、任意の数のカスタム条件を設定できます。

コミュニティ条件の定義#

"import""require""node""node-addons""default"以外の条件文字列は、デフォルトではNode.jsコアで実装されているものを除いて無視されます。

他のプラットフォームでは他の条件が実装される可能性があり、ユーザー条件は--conditions / -C フラグを介してNode.jsで有効にすることができます。

カスタムパッケージ条件は、正しい使用方法を確保するために明確な定義が必要です。そのため、エコシステムの調整に役立つ、一般的な既知のパッケージ条件とその厳密な定義のリストを以下に示します。

  • "types" - 型システムが指定されたエクスポートの型ファイルを取得するために使用できます。 *この条件は常に最初に含める必要があります。*
  • "browser" - 任意のWebブラウザ環境。
  • "development" - 開発専用の環境エントリポイントを定義するために使用できます。たとえば、開発モードで実行する場合、より良いエラーメッセージなどの追加のデバッグコンテキストを提供します。 *常に"production"と相互に排他的である必要があります。*
  • "production" - 本番環境のエントリポイントを定義するために使用できます。 *常に"development"と相互に排他的である必要があります。*

他のランタイムについては、プラットフォーム固有の条件キーの定義は、WinterCGによってRuntime Keys提案仕様で管理されています。

このセクションのNode.jsドキュメントにプルリクエストを作成することで、このリストに新しい条件定義を追加できます。ここで新しい条件定義をリストするための要件は次のとおりです。

  • 定義は、すべての開発者にとって明確で曖昧さがありません。
  • 条件が必要な理由のユースケースを明確に説明する必要があります。
  • 既存の実装使用例が十分にある必要があります。
  • 条件名は、他の条件定義や広く使用されている条件と競合してはなりません。
  • 条件定義のリストは、そうでなければ不可能なエコシステムへの調整上の利点を提供する必要があります。たとえば、これは必ずしも企業固有またはアプリケーション固有の条件の場合ではありません。
  • 条件は、Node.jsユーザーがNode.jsコアドキュメントに含まれていると期待するようなものでなければなりません。"types"条件は良い例です。これはRuntime Keys提案には実際には属しませんが、Node.jsドキュメントには適しています。

上記の定義は、適宜専用の条件レジストリに移行される可能性があります。

パッケージ名を使用したパッケージへの自己参照#

パッケージ内では、パッケージのpackage.json "exports" フィールドで定義された値は、パッケージ名を使用して参照できます。たとえば、package.jsonが次のようになっていると仮定します。

// package.json
{
  "name": "a-package",
  "exports": {
    ".": "./index.mjs",
    "./foo.js": "./foo.js"
  }
} 

その後、*そのパッケージ内の*任意のモジュールは、パッケージ自体のエクスポートを参照できます。

// ./a-module.mjs
import { something } from 'a-package'; // Imports "something" from ./index.mjs. 

自己参照は、package.json"exports"がある場合にのみ使用でき、"exports"package.json内)が許可するもののみをインポートできます。そのため、前のパッケージを考えると、以下のコードはランタイムエラーを生成します。

// ./another-module.mjs

// Imports "another" from ./m.mjs. Fails because
// the "package.json" "exports" field
// does not provide an export named "./m.mjs".
import { another } from 'a-package/m.mjs'; 

自己参照は、ESモジュールとCommonJSモジュールの両方でrequireを使用する場合にも使用できます。たとえば、このコードも機能します。

// ./a-module.js
const { something } = require('a-package/foo.js'); // Loads from ./foo.js. 

最後に、スコープ付きパッケージでも自己参照が機能します。たとえば、このコードも機能します。

// package.json
{
  "name": "@my/package",
  "exports": "./index.js"
} 
// ./index.js
module.exports = 42; 
// ./other.js
console.log(require('@my/package')); 
$ node other.js
42 

デュアルCommonJS/ESモジュールパッケージ#

Node.jsでのESモジュールのサポートが導入される前は、パッケージ作成者がパッケージにCommonJSとESモジュールのJavaScriptソースの両方を含め、package.json"main"でCommonJSのエントリポイントを指定し、package.json"module"でESモジュールのエントリポイントを指定することが一般的なパターンでした。これにより、Node.jsはCommonJSのエントリポイントを実行でき、バンドラーなどのビルドツールはESモジュールのエントリポイントを使用できました。これは、Node.jsが最上位の"module"フィールドを無視していた(そして今も無視している)ためです。

Node.jsは現在ESモジュールのエントリポイントを実行でき、パッケージにはCommonJSとESモジュールのエントリポイントの両方が含まれることができます('pkg''pkg/es-module'などの別々の指定子を使用するか、条件付きエクスポートを介して同じ指定子で両方を使用します)。"module"がバンドラーのみで使用されるシナリオ、またはESモジュールファイルがNode.jsによる評価の前にその場でCommonJSに変換されるシナリオとは異なり、ESモジュールエントリポイントによって参照されるファイルはESモジュールとして評価されます。

デュアルパッケージの危険性#

アプリケーションがCommonJSとESモジュールの両方のソースを提供するパッケージを使用している場合、パッケージの両方のバージョンが読み込まれると、特定のバグが発生するリスクがあります。この可能性は、const pkgInstance = require('pkg')によって作成されたpkgInstanceは、import pkgInstance from 'pkg'(または'pkg/module'のような代替メインパス)によって作成されたpkgInstanceとは異なるという事実から生じます。これは、「デュアルパッケージの危険性」であり、同じパッケージの2つのバージョンが同じランタイム環境で読み込まれる可能性があります。アプリケーションまたはパッケージが意図的に両方のバージョンを直接読み込むことはまれですが、アプリケーションが1つのバージョンを読み込み、アプリケーションの依存関係がもう1つのバージョンを読み込むことは一般的です。この危険性は、Node.jsがCommonJSとESモジュールの混在をサポートしており、予期しない動作につながる可能性があるため発生します。

パッケージのメインエクスポートがコンストラクターの場合、2つのバージョンによって作成されたインスタンスのinstanceof比較はfalseを返し、エクスポートがオブジェクトの場合、一方に追加されたプロパティ(pkgInstance.foo = 3など)は他方では存在しません。これは、それぞれすべてのCommonJSまたはすべてのESモジュール環境でのimportおよびrequireステートメントの動作とは異なり、ユーザーにとって驚くべきものです。また、Babelesmなどのツールによるトランスパイルを使用する場合にユーザーが慣れている動作とも異なります。

危険性を回避または最小限に抑えながらデュアルパッケージを作成する#

まず、前のセクションで説明した危険性は、パッケージにCommonJSとESモジュールの両方のソースが含まれており、両方のソースがNode.jsで使用できるように提供されている場合に発生します。これは、個別のメインエントリポイントまたはエクスポートされたパスを介して行われます。代わりに、パッケージは、Node.jsの任意のバージョンがCommonJSソースのみを受け取り、パッケージに含まれる可能性のある個別のESモジュールソースは、ブラウザなどの他の環境のみを対象とするように記述できます。このようなパッケージは、importはCommonJSファイルを参照できるため、Node.jsの任意のバージョンで使用できます。ただし、ESモジュール構文を使用するメリットは得られません。

パッケージは、破壊的変更のバージョンのバンプでCommonJS構文からESモジュール構文に切り替えることもできます。これには、パッケージの最新バージョンがESモジュールをサポートするバージョンのNode.jsでのみ使用できるという欠点があります。

すべてのパターンにはトレードオフがありますが、次の条件を満たす2つの主要なアプローチがあります。

  1. パッケージはrequireimportの両方で利用できます。
  2. パッケージは、現在のNode.jsと、ESモジュールをサポートしていない古いバージョンのNode.jsの両方で使用できます。
  3. パッケージのメインエントリポイント(例:'pkg')は、requireを使用してCommonJSファイルに解決することも、importを使用してESモジュールファイルに解決することもできます。(エクスポートされたパス(例:'pkg/feature')についても同様です。)
  4. パッケージは名前付きエクスポートを提供します(例:import { name } from 'pkg'ではなくimport pkg from 'pkg'; pkg.name)。
  5. パッケージは、ブラウザなどの他のESモジュール環境で使用できる可能性があります。
  6. 前のセクションで説明した危険性は、回避または最小限に抑えられています。
アプローチ #1: ES モジュールラッパーを使用する#

パッケージをCommonJSで記述するか、ESモジュールソースをCommonJSにトランスパイルし、名前付きエクスポートを定義するESモジュールラッパーファイルを作成します。条件付きエクスポートを使用すると、ESモジュールラッパーは`import`に使用され、CommonJSエントリポイントは`require`に使用されます。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./wrapper.mjs",
    "require": "./index.cjs"
  }
} 

上記の例では、明示的な拡張子`.mjs`と`.cjs`を使用しています。ファイルが`.js`拡張子を使用する場合、「`type": "module"`」により、そのようなファイルはESモジュールとして扱われ、「`type": "commonjs"`」の場合と同様にCommonJSとして扱われます。有効化を参照してください。

// ./node_modules/pkg/index.cjs
exports.name = 'value'; 
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name; 

この例では、`import { name } from 'pkg'`の`name`は、`const { name } = require('pkg')`の`name`と同じシングルトンです。したがって、2つの`name`を比較すると`===`は`true`を返し、異なる指定子の危険性が回避されます。

モジュールが単なる名前付きエクスポートのリストではなく、`module.exports = function () { ... }`のような一意の関数またはオブジェクトエクスポートを含んでいる場合、または`import pkg from 'pkg'`パターンのラッパーのサポートが必要な場合、ラッパーは代わりに、オプションで名前付きエクスポートと共にデフォルトをエクスポートするように記述されます。

import cjsModule from './index.cjs';
export const name = cjsModule.name;
export default cjsModule; 

このアプローチは、次のいずれかのユースケースに適しています。

  • パッケージは現在CommonJSで記述されており、作成者はESモジュール構文にリファクタリングしたくありませんが、ESモジュールコンシューマー向けに名前付きエクスポートを提供したいと考えています。
  • パッケージには、それに依存する他のパッケージがあり、エンドユーザーは、このパッケージとそれらの他のパッケージの両方をインストールする可能性があります。たとえば、`utilities`パッケージはアプリケーションで直接使用され、`utilities-plus`パッケージは`utilities`にいくつかの関数を追加します。ラッパーは基になるCommonJSファイルをエクスポートするため、`utilities-plus`がCommonJS構文またはESモジュール構文で記述されているかどうかは問題ありません。どちらの方法でも機能します。
  • パッケージは内部状態を保存しており、パッケージ作成者は状態管理を分離するためにパッケージをリファクタリングしたくありません。次のセクションを参照してください。

コンシューマー向けの条件付きエクスポートを必要としないこのアプローチのバリアントは、たとえば`"./module"`というエクスポートを追加して、パッケージのすべてのESモジュール構文バージョンのポイントにすることです。これは、依存関係などによって、CommonJSバージョンがアプリケーションのどこにもロードされないことが確実なユーザーによって`import 'pkg/module'`を使用して使用できます。または、CommonJSバージョンをロードできますが、ESモジュールバージョンに影響を与えない場合(たとえば、パッケージがステートレスなため)。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./wrapper.mjs"
  }
} 
アプローチ #2: 状態を分離する#

`package.json`ファイルは、別々のCommonJSとESモジュールエントリポイントを直接定義できます。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs"
  }
} 

これは、パッケージのCommonJSバージョンとESモジュールバージョンが同等である場合、たとえば、一方が他方のトランスパイルされた出力である場合、およびパッケージの状態管理が注意深く分離されている場合(またはパッケージがステートレスである場合)に行うことができます。

状態が問題になる理由は、パッケージのCommonJSバージョンとESモジュールバージョンの両方がアプリケーション内で使用される可能性があるためです。たとえば、ユーザーのアプリケーションコードはESモジュールバージョンを`import`する可能性があり、依存関係はCommonJSバージョンを`require`します。これが発生した場合、パッケージの2つのコピーがメモリにロードされ、したがって2つの別々の状態が存在します。これは、トラブルシューティングが困難なバグを引き起こす可能性があります。

ステートレスなパッケージを作成すること(JavaScriptの`Math`がパッケージであれば、そのすべてのメソッドが静的であるためステートレスです)に加えて、状態を分離して、潜在的にロードされたパッケージのCommonJSインスタンスとESモジュールインスタンス間で共有できるようにするいくつかの方法があります。

  1. 可能であれば、インスタンス化されたオブジェクト内にすべての状態を含めます。たとえば、JavaScriptの`Date`は状態を含めるためにインスタンス化する必要があります。パッケージであれば、次のように使用されます。

    import Date from 'date';
    const someDate = new Date();
    // someDate contains state; Date does not 

    `new`キーワードは必要ありません。パッケージの関数は、新しいオブジェクトを返すか、渡されたオブジェクトを変更して、状態をパッケージの外部に保持できます。

  2. CommonJSバージョンとESモジュールバージョンのパッケージ間で共有される1つ以上のCommonJSファイルに状態を分離します。たとえば、CommonJSとESモジュールエントリポイントがそれぞれ`index.cjs`と`index.mjs`の場合。

    // ./node_modules/pkg/index.cjs
    const state = require('./state.cjs');
    module.exports.state = state; 
    // ./node_modules/pkg/index.mjs
    import state from './state.cjs';
    export {
      state,
    }; 

    アプリケーションで`require`と`import`の両方を使用して`pkg`が使用されている場合でも(たとえば、アプリケーションコードで`import`を使用して、依存関係によって`require`を使用して)、`pkg`の各参照には同じ状態が含まれます。そして、どちらのモジュールシステムからもその状態を変更すると、両方に適用されます。

パッケージのシングルトンにアタッチするプラグインは、CommonJSシングルトンとESモジュールシングルトンの両方に個別にアタッチする必要があります。

このアプローチは、次のいずれかのユースケースに適しています。

  • パッケージは現在ESモジュール構文で記述されており、パッケージ作成者はそのような構文がサポートされている場合は常にそのバージョンを使用したいと考えています。
  • パッケージはステートレスであるか、状態をそれほど困難なく分離できます。
  • パッケージには、それに依存する他の公開パッケージがない可能性が高いか、存在する場合でも、パッケージはステートレスであるか、依存関係または全体的なアプリケーション間で共有する必要がない状態を持っています。

状態を分離しても、パッケージのCommonJSバージョンとESモジュールバージョン間の可能性のある追加のコード実行のコストは依然として存在します。

以前のアプローチと同様に、コンシューマー向けの条件付きエクスポートを必要としないこのアプローチのバリアントは、たとえば`"./module"`というエクスポートを追加して、パッケージのすべてのESモジュール構文バージョンのポイントにすることです。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./index.mjs"
  }
} 

Node.js `package.json` フィールド定義#

このセクションでは、Node.jsランタイムで使用されるフィールドについて説明します。他のツール(npmなど)は、Node.jsによって無視され、ここで説明されていない追加のフィールドを使用します。

`package.json`ファイルの次のフィールドは、Node.jsで使用されます。

  • `"name"` - パッケージ内で名前付きインポートを使用する場合に関連します。パッケージマネージャーによってもパッケージ名として使用されます。
  • `"main"` - `exports`が指定されていない場合、および`exports`が導入される前のNode.jsのバージョンでは、パッケージをロードする場合のデフォルトモジュール。
  • `"packageManager"` - パッケージへの貢献時に推奨されるパッケージマネージャー。Corepackシムによって活用されます。
  • `"type"` - `.js`ファイルをCommonJSまたはESモジュールとしてロードするかどうかを決定するパッケージの種類。
  • `"exports"` - パッケージエクスポートと条件付きエクスポート。存在する場合、パッケージ内からロードできるサブモジュールを制限します。
  • `"imports"` - パッケージ自体のモジュールで使用するためのパッケージインポート。

`"name"`#

{
  "name": "package-name"
} 

`"name"`フィールドは、パッケージの名前を定義します。 *npm*レジストリに公開するには、特定の要件を満たす名前が必要です。

`"name"`フィールドは、`"exports"`フィールドに加えて、名前を使用してパッケージを参照するために使用できます。

`"main"`#

{
  "main": "./index.js"
} 

`"main"`フィールドは、`node_modules`ルックアップを介して名前でインポートされた場合のパッケージのエントリポイントを定義します。その値はパスです。

パッケージに`"exports"`フィールドがある場合、名前でパッケージをインポートする際に、`"main"`フィールドよりも優先されます。

また、パッケージディレクトリが`require()`を介してロードされる場合に使用されるスクリプトも定義します。

// This resolves to ./path/to/directory/index.js.
require('./path/to/directory'); 

`"packageManager"`#

安定性: 1 - 試験段階

{
  "packageManager": "<package manager name>@<version>"
} 

`"packageManager"`フィールドは、現在のプロジェクトで作業する際に使用するパッケージマネージャーを定義します。サポートされているパッケージマネージャーのいずれかに設定でき、チームがNode.js以外のものをインストールする必要なく、まったく同じパッケージマネージャーバージョンを使用することを保証します。

このフィールドは現在実験的であり、オプトインする必要があります。Corepackページで手順の詳細を確認してください。

`"type"`#

`"type"`フィールドは、その`package.json`ファイルを最も近い親として持つすべての`.js`ファイルにNode.jsが使用するモジュール形式を定義します。

最も近い親の`package.json`ファイルに最上位レベルのフィールド`"type"`があり、その値が`"module"`の場合、`.js`で終わるファイルはESモジュールとしてロードされます。

最も近い親の`package.json`は、現在のフォルダー、そのフォルダーの親などで検索したときに最初に検出された`package.json`として定義され、`node_modules`フォルダーまたはボリュームルートに到達するまで続きます。

// package.json
{
  "type": "module"
} 
# In same folder as preceding package.json
node my-app.js # Runs as ES module 

最も近い親の`package.json`に`"type"`フィールドがない場合、または`"type": "commonjs"`が含まれている場合、`.js`ファイルはCommonJSとして扱われます。ボリュームルートに到達し、`package.json`が見つからない場合、`.js`ファイルはCommonJSとして扱われます。

最も近い親の`package.json`に`"type": "module"`が含まれている場合、`.js`ファイルの`import`ステートメントはESモジュールとして扱われます。

// my-app.js, part of the same example as above
import './startup.js'; // Loaded as ES module because of package.json 

`"type"`フィールドの値に関係なく、`.mjs`ファイルは常にESモジュールとして扱われ、`.cjs`ファイルは常にCommonJSとして扱われます。

`"exports"`#

{
  "exports": "./index.js"
} 

`"exports"`フィールドを使用すると、`node_modules`ルックアップまたはそれ自身の名前への自己参照を介してロードされた名前でインポートされた場合のパッケージのエントリポイントを定義できます。これは、Node.js 12以降で`"main"`の代替としてサポートされており、サブパスエクスポート条件付きエクスポートをサポートしながら、内部のエクスポートされていないモジュールをカプセル化できます。

条件付きエクスポートは`"exports"`内でも使用して、環境ごとに異なるパッケージエントリポイントを定義できます。これには、パッケージが`require`を介して参照されるか、`import`を介して参照されるかも含まれます。

"exports" で定義されているすべてのパスは、./ から始まる相対ファイルURLでなければなりません。

"imports"#

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
} 

imports フィールドのエントリは、# から始まる文字列でなければなりません。

パッケージインポートは、外部パッケージへのマッピングを許可します。

このフィールドは、現在のパッケージに対するサブパスインポートを定義します。