套件匯出

套件的 package.json 中的 exports 欄位允許宣告在使用模組要求時,例如 import "package"import "package/sub/path",應該使用哪個模組。它取代了預設實作,分別傳回 main 欄位或 index.js 檔案,用於 "package""package/sub/path" 的檔案系統查詢。

當指定 exports 欄位時,只有這些模組要求可用。任何其他要求都會導致 ModuleNotFound 錯誤。

一般語法

一般來說,exports 欄位應包含一個物件,其中每個屬性都指定模組要求的子路徑。對於上述範例,可以使用下列屬性:"." 表示 import "package""./sub/path" 表示 import "package/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" }(當 redgreen 條件都設定時,會使用第一個屬性)

在每個鍵都是子路徑的物件中,屬性(子路徑)的順序並不重要。會優先使用較具體的路徑,而非較不具體的路徑。

範例:{ "./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 欄位優先於其他套件項目欄位,例如 mainmodulebrowser 或自訂欄位。

支援

功能支援
"." 屬性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 就不會導致錯誤。

條件

參照語法

根據用於參照模組的語法設定這些條件之一

條件說明支援
import請求來自 ESM 語法或類似語法。Node.js、webpack、rollup、esinstall(1)、wmr(1)
require請求來自 CommonJs/AMD 語法或類似語法。Node.js、webpack、rollup、esinstall(1)、wmr(1)
樣式要求來自樣式表參考。
sass要求來自 sass 樣式表參考。
資源要求來自資源參考。
指令碼要求來自沒有模組系統的正常指令碼標籤。

這些條件也可能另外設定

條件說明支援
模組允許參考 JavaScript 的所有模組語法都支援 ESM。
(僅與 importrequire 結合使用)
webpack、rollup、wmr
esmodules始終由支援的工具設定。wmr
類型要求來自有興趣於類型宣告的 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 條件

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

資源

下列語法將設定 asset 條件

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

腳本

下列語法將設定 script 條件

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

script 應僅在不支援模組系統時設定。當腳本由支援 CommonJs 的系統預處理時,應改為設定 require

在尋找可注入 HTML 頁面中為腳本標籤的 javascript 檔案時,應使用此條件,且無需額外的預處理。

最佳化

下列條件設定為各種最佳化

條件說明支援
production在生產環境中。
不應包含任何開發工具。
webpack
development在開發環境中。
應包含開發工具。
webpack

注意:由於並非所有人都支援 productiondevelopment,因此在未設定任何條件時,不應做出任何假設。

目標環境

下列條件會根據目標環境設定

條件說明支援
browser程式碼將在瀏覽器中執行。webpack、esinstall、wmr
electron程式碼將在 electron 中執行。(1)webpack
worker程式碼將在 (Web)Worker 中執行。(1)webpack
worklet程式碼將在 Worklet 中執行。(1)-
節點程式碼將在 Node.js 中執行。Node.js、webpack、wmr(2)
deno程式碼將在 Deno 中執行。-
react-native程式碼將在 react-native 中執行。-

(1) electronworkerworklet 會與 nodebrowser 結合使用,具體取決於情境。

(2) 這設定為瀏覽器目標環境。

由於每個環境都有多個版本,因此套用下列準則

  • node:請參閱 engines 欄位以取得相容性。
  • browser:與當前規格和封裝發布時的第 4 階段提案相容。消費者端必須處理多重填補或轉譯。
    • 無法多重填補或轉譯的功能應謹慎使用,因為它會限制可能的用法。
  • deno:待定
  • react-native:待定

條件:預處理器和執行時期

下列條件會根據使用哪個工具預處理原始碼而設定。

條件說明支援
webpack由 webpack 處理。webpack

遺憾的是,沒有針對 Node.js 作為執行時期的 node-js 條件。這會簡化為 Node.js 建立例外。

條件:自訂

下列工具支援自訂條件

工具支援備註
Node.js使用 --conditions CLI 參數。
webpack使用 resolve.conditionNames 設定選項。
rollup針對 @rollup/plugin-node-resolve 使用 exportConditions 選項
esinstall
wmr

對於自訂條件,建議使用下列命名結構

<公司名稱>:<條件名稱>

範例:example-corp:betagoogle:internal

常見模式

所有模式都以單一 "." 項目說明套件,但也可以透過為每個項目重複模式,從多個項目進行延伸。

這些模式應當作為指南,而非嚴格的規則集。它們可以調整為個別套件。

這些模式基於下列目標/假設清單

  • 套件正在腐壞。
    • 我們假設在某個時間點,套件將不再受到維護,但會繼續使用。
    • exports 應撰寫為使用回退,以應對未知的未來案例。default 條件可以用於此。
    • 由於未來未知,我們假設環境類似於瀏覽器,且模組系統類似於 ESM。
  • 並非所有條件都受到每個工具支援。
    • 回退應當用於處理這些案例。
    • 我們假設以下回退在一般情況下有意義
      • ESM > CommonJs
      • 生產 > 開發
      • 瀏覽器 > 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 參照此套件時產生兩個執行個體,但這並無妨礙,因為套件沒有狀態。

module 條件用於在使用支援 require() 的 ESM 工具(例如在為 Node.js 進行套件時使用的套件組建器)預處理目標為節點的程式碼時進行最佳化。對於此類工具,此例外會被略過。技術上來說這是可選的,但否則套件組建器會包含套件原始碼兩次。

如果您能夠將套件狀態隔離在 JSON 檔案中,您也可以使用無狀態模式。JSON 可以從 CommonJs 和 ESM 使用,而不會使用其他模組系統污染圖形。

請注意,這裡的無狀態也表示類別執行個體不會使用 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"
  }
}

請注意,儘管對 dist-bundle.js 使用 "type": "module".js,但此檔案並非 ESM 格式。它應使用全域變數,以允許直接使用為腳本標籤。

提供開發人員工具或生產最佳化

當套件包含兩個版本時,這些模式才有意義,一個版本用於開發,另一個版本用於生產。例如,開發版本可以包含額外程式碼,以提供更好的錯誤訊息或額外的警告。

不使用 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');
}

我們偏好透過 productiondevelopment 條件靜態偵測生產/開發模式。

Node.js 允許透過 process.env.NODE_ENV 在執行時間偵測生產/開發模式,因此我們在 Node.js 中將其用作備用。同步條件匯入 ESM 不可能,而且我們不想載入套件兩次,因此我們必須對執行時間偵測使用 CommonJs。

當無法偵測模式時,我們會退回到生產版本。

提供不同版本,依據目標環境

應選擇一個備用環境,讓套件支援未來的環境有意義。一般來說,應假設類似的瀏覽器環境。

提供 Node.js、WebWorker 和瀏覽器版本

{
  "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。
  • 在 package.json 中使用 .cjstype: "commonjs",以清楚標記原始程式碼為 CommonJs。這使得工具可以靜態偵測是否使用 CommonJs 或 ESM。這對於僅支援 ESM 而非 CommonJs 的工具非常重要。
  • 套件中使用的 ESM 支援下列類型的要求
    • 支援模組要求,指向具有 package.json 的其他套件。
    • 支援相對要求,指向套件內的其他檔案。
      • 它們不得指向套件外的檔案。
    • 支援data: url 要求。
    • 預設不支援其他絕對或伺服器相對要求,但某些工具或環境可能會支援。

1 貢獻者

sokra