パッケージエクスポート

パッケージの`package.json`内の`exports`フィールドを使用すると、`import "package"`または`import "package/sub/path"`のようなモジュールリクエストを使用する場合に使用するモジュールを宣言できます。これは、`main`フィールドまたは`index.js`ファイルを`package`に対して返し、`package/sub/path`に対してファイルシステムのルックアップを行うデフォルトの実装に取って代わります。

`exports`フィールドが指定されている場合、これらのモジュールリクエストのみが使用可能です。その他のリクエストは、ModuleNotFoundエラーになります。

一般的な構文

一般的に、`exports`フィールドには、各プロパティがモジュールリクエストのサブパスを指定するオブジェクトが含まれている必要があります。上記の例では、`import "package"`には`"."`、`import "package/sub/path"`には`"./sub/path"`というプロパティを使用できます。`/`で終わるプロパティは、このプレフィックスを持つリクエストを古いファイルシステムのルックアップアルゴリズムに転送します。`*`で終わるプロパティの場合、`*`は任意の値を取ることができ、プロパティ値内の`*`は取得した値に置き換えられます。

{
  "exports": {
    ".": "./main.js",
    "./sub/path": "./secondary.js",
    "./prefix/": "./directory/",
    "./prefix/deep/": "./other-directory/",
    "./other-prefix/*": "./yet-another/*/*.js"
  }
}
モジュールリクエスト結果
package.../package/main.js
package/sub/path.../package/secondary.js
package/prefix/some/file.js.../package/directory/some/file.js
package/prefix/deep/file.js.../package/other-directory/file.js
package/other-prefix/deep/file.js.../package/yet-another/deep/file/deep/file.js
package/main.jsエラー

代替案

単一の結果を提供する代わりに、パッケージ作成者は結果のリストを提供できます。このようなシナリオでは、このリストが順番に試され、最初の有効な結果が使用されます。

注:最初の有効な結果のみが使用され、すべての有効な結果が使用されるわけではありません。

{
  "exports": {
    "./things/": ["./good-things/", "./bad-things/"]
  }
}

ここでは、`package/things/apple`は`.../package/good-things/apple`または`.../package/bad-things/apple`で見つかる可能性があります。

条件付き構文

`exports`フィールドに直接結果を提供する代わりに、パッケージ作成者はモジュールシステムが環境に関する条件に基づいて結果を選択できるようにすることができます。

この場合、条件と結果をマッピングするオブジェクトを使用する必要があります。条件はオブジェクトの順序で試されます。無効な結果を含む条件はスキップされます。条件はネストして論理ANDを作成できます。オブジェクトの最後の条件は特別な`default`条件にすることができ、これは常に一致する条件です。

{
  "exports": {
    ".": {
      "red": "./stop.js",
      "yellow": "./stop.js",
      "green": {
        "free": "./drive.js",
        "default": "./wait.js"
      },
      "default": "./drive-carefully.js"
    }
  }
}

これは次のようなものになります。

if (red && valid('./stop.js')) return './stop.js';
if (yellow && valid('./stop.js')) return './stop.js';
if (green) {
  if (free && valid('./drive.js')) return './drive.js';
  if (valid('./wait.js')) return './wait.js';
}
if (valid('./drive-carefully.js')) return './drive-carefully.js';
throw new ModuleNotFoundError();

使用可能な条件は、使用されるモジュールシステムとツールによって異なります。

略記

パッケージへの単一のエントリ(`.`)のみをサポートする必要がある場合、`{ ".": ... }`オブジェクトのネストを省略できます。

{
  "exports": "./index.mjs"
}
{
  "exports": {
    "red": "./stop.js",
    "green": "./drive.js"
  }
}

順序に関する注意事項

各キーが条件であるオブジェクトでは、プロパティの順序は重要です。条件は指定された順序で処理されます。

例:`{ "red": "./stop.js", "green": "./drive.js" }` != `{ "green": "./drive.js", "red": "./stop.js" }` (`red`と`green`の両方の条件が設定されている場合、最初のプロパティが使用されます)

各キーがサブパスであるオブジェクトでは、プロパティ(サブパス)の順序は重要ではありません。より具体的なパスの方が、それほど具体的なパスよりも優先されます。

例:`{ "./a/": "./x/", "./a/b/": "./y/", "./a/b/c": "./z" }` == `{ "./a/b/c": "./z", "./a/b/": "./y/", "./a/": "./x/" }` (順序は常に`./a/b/c` > `./a/b/` > `./a/`となります)

`exports`フィールドは、`main`、`module`、`browser`、またはカスタムなどの他のパッケージエントリフィールドよりも優先されます。

サポート

機能サポート対象
`"."`プロパティNode.js、webpack、rollup、esinstall、wmr
通常のプロパティNode.js、webpack、rollup、esinstall、wmr
`/`で終わるプロパティNode.js(1)、webpack、rollup、esinstall(2)、wmr(3)
*で終わるプロパティNode.js、webpack、rollup、esinstall
代替案Node.js、webpack、rollup、esinstall(4)
省略形のみのパスNode.js、webpack、rollup、esinstall、wmr
省略形のみの条件Node.js、webpack、rollup、esinstall、wmr
条件付き構文Node.js、webpack、rollup、esinstall、wmr
ネストされた条件付き構文Node.js、webpack、rollup、wmr(5)
条件の順序Node.js、webpack、rollup、wmr(6)
"default"条件Node.js、webpack、rollup、esinstall、wmr
パスの順序Node.js、webpack、rollup
マップされていない場合のエラーNode.js、webpack、rollup、esinstall、wmr(7)
条件とパスの混合時のエラーNode.js、webpack、rollup

(1) Node.jsでは非推奨。*を推奨します。

(2) "./"はキーとして意図的に無視されます。

(3) プロパティの値は無視され、プロパティキーがターゲットとして使用されます。キーと値が同一であるマッピングのみを有効に許可します。

(4) この構文はサポートされていますが、常に最初のエントリが使用されるため、実用的なユースケースでは使用できません。

(5) 代替の兄弟親条件へのフォールバックが正しく処理されていません。

(6) require条件オブジェクトの順序は正しく処理されていません。これは、wmrが参照構文を区別しないため、意図的なものです。

(7) "exports": "./file.js" の省略形を使用する場合、例:package/not-existing などのリクエストはそれに解決されます。省略形を使用しない場合、例:package/file.js の直接ファイルアクセスはエラーになりません。

条件

参照構文

モジュールを参照するために使用される構文に応じて、これらの条件のいずれかが設定されます。

条件説明サポート対象
importESM構文または同様のものからリクエストが発行されます。Node.js、webpack、rollup、esinstall(1)、wmr(1)
requireCommonJs/AMD構文または同様のものからリクエストが発行されます。Node.js、webpack、rollup、esinstall(1)、wmr(1)
styleスタイルシート参照からリクエストが発行されます。
sasssassスタイルシート参照からリクエストが発行されます。
assetアセット参照からリクエストが発行されます。
scriptモジュールシステムを使用しない通常のスクリプトタグからリクエストが発行されます。

これらの条件は追加で設定される可能性もあります。

条件説明サポート対象
moduleJavaScriptを参照できるすべてのモジュール構文はESMをサポートしています。
(importまたはrequireとのみ組み合わせる)
webpack、rollup、wmr
esmodulesサポートされているツールによって常に設定されます。wmr
types型宣言に関心のあるTypeScriptからリクエストが発行されます。

(1) importrequireは、参照構文に関係なく独立して設定されます。requireは常に優先順位が低くなります。

import

次の構文により、import条件が設定されます。

  • ESMにおけるESM import宣言
  • JS import()
  • HTMLにおけるHTML <script type="module">
  • HTMLにおけるHTML <link rel="preload/prefetch">
  • JS new Worker(..., { type: "module" })
  • WASM importセクション
  • ESM HMR (webpack) import.hot.accept/decline([...])
  • JS Worklet.addModule
  • エントリポイントとしてJavaScriptを使用する場合

require

次の構文により、require条件が設定されます。

  • CommonJs require(...)
  • AMD define()
  • AMD require([...])
  • CommonJs require.resolve()
  • CommonJs (webpack) require.ensure([...])
  • CommonJs (webpack) require.context
  • CommonJs HMR (webpack) module.hot.accept/decline([...])
  • HTML <script src="...">

style

次の構文により、style条件が設定されます。

  • CSS @import
  • HTML <link rel="stylesheet">

asset

次の構文により、asset条件が設定されます。

  • CSS url()
  • ESM new URL(..., import.meta.url)
  • HTML <img src="...">

script

次の構文により、script条件が設定されます。

  • HTML <script src="...">

scriptは、モジュールシステムがサポートされていない場合にのみ設定する必要があります。スクリプトがCommonJsをサポートするシステムによってプリプロセスされる場合、代わりにrequireを設定する必要があります。

この条件は、追加のプリプロセスなしでHTMLページにスクリプトタグとして挿入できるJavaScriptファイルを探す場合に使用します。

最適化

さまざまな最適化のために、次の条件が設定されます。

条件説明サポート対象
production本番環境です。
デバッグツールを含めるべきではありません。
webpack
development開発環境です。
デバッグツールを含めるべきです。
webpack

注:productiondevelopmentはすべてでサポートされているわけではないため、これらのいずれも設定されていない場合は、想定すべきではありません。

ターゲット環境

ターゲット環境に応じて、次の条件が設定されます。

条件説明サポート対象
browserコードはブラウザで実行されます。webpack、esinstall、wmr
electronコードはElectronで実行されます。(1)webpack
workerコードは(Web)ワーカーで実行されます。(1)webpack
workletコードはWorkletで実行されます。(1)-
nodeコードはNode.jsで実行されます。Node.js、webpack、wmr(2)
denoコードはDenoで実行されます。-
react-nativeコードはReact Nativeで実行されます。-

(1) electronworkerworkletは、コンテキストに応じてnodeまたはbrowserのいずれかと組み合わせて使用されます。

(2) これはブラウザのターゲット環境に対して設定されます。

各環境には複数のバージョンがあるため、次のガイドラインが適用されます。

  • node: 互換性についてはenginesフィールドを参照してください。
  • browser: パッケージの公開時点での現在の仕様とステージ4の提案と互換性があります。ポリフィルまたはトランスパイルは、利用者側で処理する必要があります。
    • ポリフィルまたはトランスパイルできない機能は、使用可能な範囲が制限されるため、注意深く使用する必要があります。
  • deno: 未定
  • react-native: 未定

条件:プリプロセッサとランタイム

ソースコードをプリプロセスするツールに応じて、次の条件が設定されます。

条件説明サポート対象
webpackwebpackによって処理されます。webpack

残念なことに、Node.jsをランタイムとして使用するNode.js用のnode-js条件はありません。これにより、Node.jsの例外の作成が簡素化されます。

条件:カスタム

次のツールはカスタム条件をサポートしています。

ツールサポート注記
Node.jsはい--conditions CLI引数を使用します。
webpackはいresolve.conditionNames 設定オプションを使用します。
rollupはい@rollup/plugin-node-resolveexportConditions オプションを使用します。
esinstallいいえ
wmrいいえ

カスタム条件には、次の命名スキームをお勧めします。

<会社名>:<条件名>

例:example-corp:betagoogle:internal

一般的なパターン

すべてのパターンは、パッケージへの単一の"."エントリで説明されていますが、各エントリに対してパターンを繰り返すことで、複数のエントリから拡張することもできます。

これらのパターンは、厳格なルールセットではなく、ガイドとして使用する必要があります。個々のパッケージに合わせて調整できます。

これらのパターンは、次の目標/前提に基づいています。

  • パッケージは腐敗しています。
    • パッケージはいつかメンテナンスされなくなりますが、使い続けられると想定しています。
    • exportsは、未知の将来のケースのためにフォールバックを使用するように記述する必要があります。default条件はそれのために使用できます。
    • 未来は不明であるため、ブラウザに似た環境とESMに似たモジュールシステムを想定しています。
  • すべてのツールですべての条件がサポートされているわけではありません。
    • フォールバックを使用してこれらのケースを処理する必要があります。
    • 一般的に、次のフォールバックは理にかなっていると想定しています。
      • ESM > CommonJs
      • Production > Development
      • Browser > node.js

パッケージの意図によっては、別のものが理にかなう場合があり、この場合、パターンをそれに合わせて調整する必要があります。例:コマンドラインツールの場合は、ブラウザのような未来とフォールバックはあまり意味がなく、この場合は、代わりにNode.jsのような環境とフォールバックを使用する必要があります。

複雑なユースケースでは、これらの条件をネストすることで、複数のパターンを組み合わせる必要があります。

ターゲット環境に依存しないパッケージ

これらのパターンは、環境固有のAPIを使用しないパッケージに適しています。

ESMバージョンのみを提供する場合

{
  "type": "module",
  "exports": "./index.js"
}

注:ESMのみを提供することは、Node.jsに制限があります。そのようなパッケージは、Node.js >= 14 でのみ、importを使用する場合にのみ機能します。require()では機能しません。

CommonJsとESMバージョンを提供する(ステートレス)

{
  "type": "module",
  "exports": {
    "node": {
      "module": "./index.js",
      "require": "./index.cjs"
    },
    "default": "./index.js"
  }
}

ほとんどのツールはESMバージョンを取得します。Node.jsはここでの例外です。require()を使用する場合は、CommonJsバージョンを取得します。これにより、require()importで参照する場合に、これらのパッケージの2つのインスタンスが作成されますが、パッケージに状態がないため、問題ありません。

module条件は、require()に対してESMをサポートするツール(Node.jsの場合のバンダラなど)を使用して、Nodeをターゲットとするコードをプリプロセスする場合の最適化として使用されます。このようなツールでは、例外はスキップされます。これは技術的にはオプションですが、そうでない場合はバンダラがパッケージのソースコードを2回含めることになります。

パッケージの状態をJSONファイルに隔離できる場合は、ステートレスパターンを使用することもできます。JSONは、他のモジュールシステムでグラフを汚染することなく、CommonJsとESMから利用できます。

ここでステートレスとは、二重モジュールインスタンス化のため2つの異なるクラスが存在する可能性があるため、instanceofを使用してクラスインスタンスをテストしないことを意味します。

CommonJsとESMバージョンの提供(ステートフル)

{
  "type": "module",
  "exports": {
    "node": {
      "module": "./index.js",
      "import": "./wrapper.js",
      "require": "./index.cjs"
    },
    "default": "./index.js"
  }
}
// wrapper.js
import cjs from './index.cjs';

export const A = cjs.A;
export const B = cjs.B;

ステートフルパッケージでは、パッケージが二度とインスタンス化されないようにする必要があります。

これはほとんどのツールでは問題ではありませんが、Node.jsはここでも例外です。Node.jsでは、常にCommonJsバージョンを使用し、ESMラッパーでESMに名前付きエクスポートを公開します。

最適化として再びmodule条件を使用します。

CommonJsバージョンのみの提供

{
  "type": "commonjs",
  "exports": "./index.js"
}

"type": "commonjs"を指定すると、CommonJsファイルを静的に検出できます。

ブラウザでの直接利用のためのバンドル済みスクリプトバージョンの提供

{
  "type": "module",
  "exports": {
    "script": "./dist-bundle.js",
    "default": "./index.js"
  }
}

"type": "module".jsdist-bundle.jsに使用しているにもかかわらず、このファイルはESM形式ではありません。スクリプトタグとして直接使用できるように、グローバル変数を使用する必要があります。

開発ツールまたは本番環境の最適化の提供

これらのパターンは、パッケージに開発用と本番用の2つのバージョンが含まれている場合に有効です。たとえば、開発バージョンには、より良いエラーメッセージや追加の警告のための追加コードを含めることができます。

Node.jsランタイム検出なし

{
  "type": "module",
  "exports": {
    "development": "./index-with-devtools.js",
    "default": "./index-optimized.js"
  }
}

development条件がサポートされている場合、開発用に拡張されたバージョンを使用します。それ以外の場合は、本番環境またはモードが不明な場合は、最適化されたバージョンを使用します。

Node.jsランタイム検出あり

{
  "type": "module",
  "exports": {
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "node": "./wrapper-process-env.cjs",
    "default": "./index-optimized.js"
  }
}
// wrapper-process-env.cjs
if (process.env.NODE_ENV !== 'development') {
  module.exports = require('./index-optimized.cjs');
} else {
  module.exports = require('./index-with-devtools.cjs');
}

productionまたはdevelopment条件を使用して、本番/開発モードの静的検出を優先します。

Node.jsでは、process.env.NODE_ENVを使用してランタイムで本番/開発モードを検出できるため、Node.jsではそれをフォールバックとして使用します。ESMの同期条件付きインポートは不可能であり、パッケージを2回ロードしたくありません。そのため、ランタイム検出にはCommonJsを使用する必要があります。

モードを検出できない場合は、本番バージョンにフォールバックします。

ターゲット環境に応じて異なるバージョンを提供する

将来の環境をサポートするために、パッケージにとって適切なフォールバック環境を選択する必要があります。一般的に、ブラウザのような環境を想定する必要があります。

Node.js、Web Worker、ブラウザバージョンの提供

{
  "type": "module",
  "exports": {
    "node": "./index-node.js",
    "worker": "./index-worker.js",
    "default": "./index.js"
  }
}

Node.js、ブラウザ、Electronバージョンの提供

{
  "type": "module",
  "exports": {
    "electron": {
      "node": "./index-electron-node.js",
      "default": "./index-electron.js"
    },
    "node": "./index-node.js",
    "default": "./index.js"
  }
}

パターンの組み合わせ

例1

これは、process.envのランタイム検出による本番と開発の使用のための最適化を持ち、CommonJsとESMの両方のバージョンを提供するパッケージの例です。

{
  "type": "module",
  "exports": {
    "node": {
      "development": {
        "module": "./index-with-devtools.js",
        "import": "./wrapper-with-devtools.js",
        "require": "./index-with-devtools.cjs"
      },
      "production": {
        "module": "./index-optimized.js",
        "import": "./wrapper-optimized.js",
        "require": "./index-optimized.cjs"
      },
      "default": "./wrapper-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}

例2

これは、Node.js、ブラウザ、Electronをサポートし、process.envのランタイム検出による本番と開発の使用のための最適化を持ち、CommonJsとESMの両方のバージョンを提供するパッケージの例です。

{
  "type": "module",
  "exports": {
    "electron": {
      "node": {
        "development": {
          "module": "./index-electron-node-with-devtools.js",
          "import": "./wrapper-electron-node-with-devtools.js",
          "require": "./index-electron-node-with-devtools.cjs"
        },
        "production": {
          "module": "./index-electron-node-optimized.js",
          "import": "./wrapper-electron-node-optimized.js",
          "require": "./index-electron-node-optimized.cjs"
        },
        "default": "./wrapper-electron-node-process-env.cjs"
      },
      "development": "./index-electron-with-devtools.js",
      "production": "./index-electron-optimized.js",
      "default": "./index-electron-optimized.js"
    },
    "node": {
      "development": {
        "module": "./index-node-with-devtools.js",
        "import": "./wrapper-node-with-devtools.js",
        "require": "./index-node-with-devtools.cjs"
      },
      "production": {
        "module": "./index-node-optimized.js",
        "import": "./wrapper-node-optimized.js",
        "require": "./index-node-optimized.cjs"
      },
      "default": "./wrapper-node-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}

複雑に見えますね。私たちは、次のような仮定をすることで、複雑さを軽減することができました。NodeのみがCommonJsバージョンを必要とし、process.envで本番/開発を検出できます。

ガイドライン

  • defaultエクスポートは避けてください。ツール間で異なる方法で処理されます。名前付きエクスポートのみを使用してください。
  • 異なる条件に対して異なるAPIまたはセマンティクスを提供しないでください。
  • ソースコードをESMとして記述し、babel、typescriptなどのツールを使用してCJSに変換します。
  • .cjsまたはpackage.jsontype: "commonjs"を使用して、ソースコードをCommonJsとして明確にマークします。これにより、CommonJsとESMのどちらが使用されているかをツールが静的に検出できるようになります。これは、ESMのみをサポートし、CommonJsをサポートしていないツールにとって重要です。
  • パッケージで使用されるESMは、次のタイプの要求をサポートします。
    • モジュール要求がサポートされており、package.jsonを持つ他のパッケージを指します。
    • 相対要求がサポートされており、パッケージ内の他のファイルを指します。
      • パッケージ外のファイルを参照することはできません。
    • data: URL要求がサポートされています。
    • 他の絶対パスまたはサーバー相対パス要求は、デフォルトではサポートされていませんが、一部のツールや環境ではサポートされている場合があります。

1 貢献者

sokra