包导出

包的 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)
style请求来自样式表引用。
sass请求来自 sass 样式表引用。
asset请求来自资产引用。
script请求来自没有模块系统的普通脚本标签。

这些条件也可能被额外设置

条件描述支持
module所有允许引用 JavaScript 的模块语法都支持 ESM。
(仅与 importrequire 结合使用)
webpack、rollup、wmr
esmodules始终由支持的工具设置。wmr
types请求来自对类型声明感兴趣的 TypeScript。

(1) importrequire 都独立于引用语法设置。require 始终具有较低的优先级。

import

以下语法将设置 import 条件

  • ESM 中的 ESM import 声明
  • JS import() 表达式
  • HTML 中的 <script type="module">
  • 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 文件时,应使用此条件。

优化

以下条件用于各种优化

条件描述支持
生产在生产环境中。
不应包含任何调试工具。
webpack
开发在开发环境中。
应包含调试工具。
webpack

注意:由于 productiondevelopment 不被所有人支持,因此在没有设置任何这些条件时,不应做出任何假设。

目标环境

以下条件根据目标环境设置

条件描述支持
浏览器代码将在浏览器中运行。webpack、esinstall、wmr
electron代码将在 electron 中运行。(1)webpack
worker代码将在(Web)Worker 中运行。(1)webpack
worklet代码将在 Worklet 中运行。(1)-
node代码将在 Node.js 中运行。Node.js、webpack、wmr(2)
deno代码将在 Deno 中运行。-
react-native代码将在 react-native 中运行。-

(1) electronworkerworkletnodebrowser 结合使用,具体取决于上下文。

(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-resolveexportConditions 选项
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 条件用作优化,用于使用支持 ESM 的工具(如捆绑器,在为 Node.js 捆绑时)预处理以 node 为目标的代码时。对于这样的工具,异常会被跳过。这在技术上是可选的,但捆绑器否则会包含两次包源代码。

如果能够将包状态隔离在 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"
  }
}

请注意,尽管使用"type": "module".js用于dist-bundle.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