包导出

包的 package.json 中的 exports 字段允许声明在使用 import "package"import "package/sub/path" 等模块请求时应使用哪个模块。它取代了默认实现,默认实现会为 package 返回 main 字段或 index.js 文件,并为 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": {
    ".": ["-bad-specifier-", "./non-existent.js", "./existent.js"]
  }
}

Webpack 5.94.0+ 现在会抛出错误,因为找不到 non-existent.js,而之前的行为会解析到 existent.js

条件语法

包作者可以根据环境条件,让模块系统选择一个结果,而不是直接在 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 17 中移除。请改用 *

(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请求源自不带模块系统的普通 script 标签。-

这些条件也可能额外设置

条件描述支持者
module所有允许引用 JavaScript 的模块语法都支持 ESM。
(仅与 importrequire 结合使用)
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 页面作为 script 标签而无需额外预处理的 JavaScript 文件时使用。

优化

为各种优化设置以下条件

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

注意:由于并非所有工具都支持 productiondevelopment,因此当这些条件均未设置时,不应做出任何假设。

目标环境

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

条件描述支持者
browser代码将在浏览器中运行。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) electronworkerworklet 会根据上下文与 nodebrowser 结合使用。

(2) 这适用于浏览器目标环境。

由于每个环境都有多个版本,因此适用以下准则

  • node:兼容性请参见 engines 字段。
  • browser:与发布包时的当前规范和阶段 4 提案兼容。polyfill 和转译必须在消费者端处理。
    • 无法进行 polyfill 或转译的特性应谨慎使用,因为它会限制可能的用法。
  • 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 打包的打包器)预处理面向 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"
  }
}

请注意,尽管 dist-bundle.js 使用了 "type": "module".js,但此文件并非 ESM 格式。它应使用全局变量,以便可以直接作为 script 标签使用。

提供开发工具或生产优化

当一个包包含两个版本(一个用于开发,一个用于生产)时,这些模式是有意义的。例如,开发版本可以包含用于更好的错误消息或额外警告的附加代码。

不带 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