Webpack Package exports
package.json
中的 exports
字段允许声明在使用模块请求(如import "package"
或 import "package/sub/path"
)时应使用哪个模块。它替代了 default 实现,该实现返回 "package" 的 main
字段或index.js文件,以及对"package/sub/path"
进行文件系统查找。
当指定 exports
字段时,只有这些模块请求可用。任何其他请求将导致ModuleNotFound错误。
General syntax
一般情况下,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"
}
}
Module request | Result |
---|---|
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
|
Error |
Alternatives
与提供单个结果不同,包作者可以提供一系列结果。在这种情况下,将按顺序尝试此列表,并使用第一个有效结果。
注意:只使用第一个有效结果,而不是所有有效结果。
举例:
{
"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();
可用的条件取决于所使用的模块系统和工具。
Abbreviation
当仅支持对包中的单个条目("."
)的访问时,可以省略 { ".": ... }
对象嵌套:
{
"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
或自定义字段。
Support
Feature | Supported by |
---|---|
"." property |
Node.js, webpack, rollup, esinstall, wmr |
normal property | Node.js, webpack, rollup, esinstall, wmr |
property ending with /
|
Node.js(1), webpack, rollup, esinstall(2), wmr(3) |
property ending with *
|
Node.js, webpack, rollup, esinstall |
Alternatives | Node.js, webpack, rollup, |
Abbreviation only path | Node.js, webpack, rollup, esinstall, wmr |
Abbreviation only conditions | Node.js, webpack, rollup, esinstall, wmr |
Conditional syntax | Node.js, webpack, rollup, esinstall, wmr |
Nested conditional syntax | Node.js, webpack, rollup, wmr(5) |
Conditions Order | Node.js, webpack, rollup, wmr(6) |
"default" condition |
Node.js, webpack, rollup, esinstall, wmr |
Path Order | Node.js, webpack, rollup |
Error when not mapped | Node.js, webpack, rollup, esinstall, wmr(7) |
Error when mixing conditions and paths | Node.js, webpack, rollup |
(1) 在Node.js中已弃用,应优先使用*
。
(2) "./"
被有意忽略作为键。
(3) 属性值被忽略,属性键被用作目标。实际上,只允许键和值相同的映射。
(4) 语法是支持的,但始终使用第一个条目,这使其在任何实际用例中都无法使用。
(5) 回退到替代的同级父级条件处理错误。
(6) 对于require
条件对象,对象顺序处理不正确。这是有意的,因为wmr不区分引用语法。
(7) 当使用"exports"
: "./file.js"
缩写时,任何请求,例如package/not-existing
将解析到该文件。当不使用缩写时,直接访问文件,例如 package/file.js
将不会导致错误。
条件
引用语法
根据用于引用模块的语法之一设置以下条件:
Condition | Description | Supported by |
---|---|---|
import
|
Request is issued from ESM syntax or similar. | Node.js, webpack, rollup, esinstall(1), wmr(1) |
require
|
Request is issued from CommonJs/AMD syntax or similar. | Node.js, webpack, rollup, esinstall(1), wmr(1) |
style
|
Request is issued from a stylesheet reference. | |
sass
|
Request is issued from a sass stylesheet reference. | |
asset
|
Request is issued from a asset reference. | |
script
|
Request is issued from a normal script tag without module system. |
这些条件也可以额外设置:
Condition | Description | Supported by |
---|---|---|
module
|
All module syntax that allows to reference javascript supports ESM.
(only combined with import or require ) |
webpack, rollup, wmr |
esmodules
|
Always set by supported tools. | wmr |
types
|
Request is issued from typescript that is interested in type declarations. |
(1)import
和 require
都是独立于引用语法设置的。 require
始终具有较低的优先级。
import
以下语法将设置导入条件:
ESM ESM 中的
import
申报- JS
import()
表达式 - HTML
<script type="module">
在 HTML 中 - HTML
<link rel="preload/prefetch">
在 HTML 中 - 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
The following syntax will set the asset condition:
- CSS
url()
- ESM
new URL(..., import.meta.url)
- HTML
<img src="...">
script
以下语法将设置脚本条件:
- HTML
<script src="...">
仅当不支持模块系统时才应设置 script
。当 script
由支持 CommonJs 的系统预处理时,它应该改为设置 require
。
在寻找可以作为脚本标记注入 HTML 页面而无需额外预处理的 javascript 文件时,应使用此条件。
Optimizations
The following conditions are set for various optimizations:
Condition | Description | Supported by |
---|---|---|
production
|
在生产环境中,不应包括开发工具 | webpack |
development
|
在开发环境中,应该包括开发工具 | webpack |
Note:
由于
production
和development
不是每个人都支持的,所以当这些都没有设置时,不要做任何假设。
Target environment
根据目标环境设置以下条件:
Condition | Description | Supported by |
---|---|---|
browser
|
代码将在浏览器中运行。 | webpack, esinstall, wmr |
electron | 代码将在electron中运行。 | webpack |
worker | 代码将在worker中运行。 | webpack |
worklet
|
代码将在worklet中运行。 | - |
node
|
代码将在node中运行。 | Node.js, webpack, wmr |
deno
|
代码将在deno中运行。 | - |
react-native | 代码将在react-native中运行。 | - |
(1)根据上下文,electron
、worker
和worklet
可以与 node
和 browser
结合使用。
(2)这是为浏览器目标环境设置的。
由于每个环境都有多个版本,因此适用以下指导原则:
- node:请参阅引擎字段以了解兼容性。
浏览器:在发布包时兼容当前的规范和第4阶段建议。Polyfilling职责。编译必须在消费者端处理。不可能填充或转译的特性应该小心使用,因为它限制了可能的使用。
-
deno
: TBD -
react-native
: TBD
Conditions: Preprocessor and runtimes
根据对源代码进行预处理的工具设置以下条件。
Condition | Description | Supported by |
---|---|---|
webpack
|
由webpack处理 | webpack |
遗憾的是,Node.js运行时没有 node-js
条件。这将简化为Node.js创建异常。
Conditions: Custom
以下工具支持自定义条件:
Tool | Supported | Notes |
---|---|---|
Node.js | yes | Use --conditions CLI argument. |
webpack | yes | Use resolve.conditionNames configuration option. |
rollup | yes | Use exportConditions option for @rollup/plugin-node-resolve
|
esinstall | no | |
wmr | no |
对于自定义条件,建议使用以下命名模式:
<company-name>:<condition-name>
例如:example-corp:beta
, google:internal
。
Common patterns
所有模式都用包中的单个"."
条目来解释,但是它们也可以从多个条目扩展,通过为每个条目重复模式。
这些模式应该用作指南,而不是严格的规则集。它们可以适应不同的软件包。
这些模式基于以下目标/假设列表:
- 我们假设在某些时候不再维护包,但是它们继续被使用。应该编写导出,以便为未来未知的情况使用回退。可以使用
default
条件。由于未来是未知的,我们假设一个类似浏览器的环境和类似ESM的模块系统。 - 并非所有工具都支持所有条件。应该使用回退来处理这些情况。我们假设下面的回退通常是有意义的:
- ESM > CommonJs
- Production > Development
- Browser > node.js
根据package的意图,可能有些其他的东西是有意义的,在这种情况下应该采用模式。示例:对于命令行工具来说,类似浏览器的未来和回退并没有多大意义,在这种情况下,应该使用类似node.js的环境和回退。
对于复杂的用例,需要通过嵌套这些条件来组合多个模式。
Target environment independent packages
这些模式对于不使用特定于环境的api的包是有意义的。
Providing only an ESM version
{
"type": "module",
"exports": "./index.js"
}
Note:
只提供ESM对node.js来说是有限制的。这样的包只能在Node.js >= 14中工作,并且只能在使用
import
时工作。它不能与 require()
一起工作。
Providing CommonJs and ESM version (stateless)
{
"type": "module",
"exports": {
"node": {
"module": "./index.js",
"require": "./index.cjs"
},
"default": "./index.js"
}
}
大多数工具都是ESM版本。Node.js在这里是个例外。当使用 require()
时,它得到一个CommonJs版本。当使用 require()
和 import
引用包时,这将导致这些包的两个实例,但这不会造成损害,因为包没有状态。
当使用支持ESM的 require()
工具预处理面向节点的代码时, module
条件被用作优化(就像绑定器一样,在为Node.js绑定时)。对于这样的工具,将跳过异常。这在技术上是可选的,但如果不这样做,打包器将包含两次包源代码。
如果能够在JSON文件中隔离包状态,也可以使用无状态模式。JSON可以从CommonJs和ESM中使用,而不会用其他模块系统污染图形。
请注意,这里的无状态还意味着不使用instanceof测试类实例,因为由于双模块实例化,可能存在两个不同的类。
Providing CommonJs and ESM version (stateful)
{
"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
条件作为优化。
Providing only a CommonJs version
{
"type": "commonjs",
"exports": "./index.js"
}
提供"type": "commonjs"
有助于静态检测commonjs文件。
Providing a bundled script version for direct browser consumption
{
"type": "module",
"exports": {
"script": "./dist-bundle.js",
"default": "./index.js"
}
}
Note:
尽管在 dist-bundle.js
中使用了 "type": "module"
和.js
,但这个文件不是ESM格式的。它应该使用全局变量来允许直接使用脚本标记。
Providing devtools or production optimizations
当一个package包含两个版本,一个用于开发,一个用于生产时,这些模式是有意义的。例如,开发版本可以包含额外的代码,以提供更好的错误信息或额外的警告。
Without Node.js runtime detection
{
"type": "module",
"exports": {
"development": "./index-with-devtools.js",
"default": "./index-optimized.js"
}
}
当开发条件得到支持时,我们使用版本增强进行开发。否则,在生产中或模式未知时,我们使用优化版本。
With Node.js runtime detection
{
"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是不可能的,我们不想加载包两次,所以我们必须使用CommonJs来进行运行时检测。
当无法检测到模式时,我们会退回到生产版本。
Providing different versions depending on target environment
应该选择一个对包支持未来环境有意义的回退环境。通常应该假设一个类似浏览器的环境。
Providing Node.js, WebWorker and browser versions
{
"type": "module",
"exports": {
"node": "./index-node.js",
"worker": "./index-worker.js",
"default": "./index.js"
}
}
Providing Node.js, browser and electron versions
{
"type": "module",
"exports": {
"electron": {
"node": "./index-electron-node.js",
"default": "./index-electron.js"
},
"node": "./index-node.js",
"default": "./index.js"
}
}
Combining patterns
Example 1
这是一个package的例子,它对生产和开发使用进行了优化,并对 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"
}
}
Example 2
这是一个支持Node.js、浏览器和电子的包的例子,它对生产和开发使用进行了优化,并对 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
检测生产/开发。
Guidelines
避免
default
导出。不同工具之间的处理方式不同。只使用命名的导出。- 永远不要为不同的条件提供不同的api或语义。
- 将源代码写成ESM,然后通过babel、typescript或类似的工具编译成CJS。
- 在package中使用
.cjs
或 type:"commonjs"
。将源代码清晰地标记为CommonJs。这使得如果使用CommonJs或ESM,工具可以静态地检测到它。这对于只支持ESM而不支持CommonJs的工具来说非常重要。 - 包中使用的ESM支持以下类型的请求:支持模块请求,指向其他包,使用package.json.relative请求,指向包中的其他文件。它们不能指向包外的文件。数据:支持url请求。默认情况下不支持其他绝对请求或服务器相对请求,但某些工具或环境可能支持它们。
Further Reading
更多建议: