Skip to content

每日一报

package.json 的 exports 字段

npm 包的 package.json 中包含的 exports 字段只是为了告知 node,依赖方的不同方式加载 npm 包所对应的不同入口模块的位置。

js
import demo from 'demo';
console.log(demo);
js
const demo = require('demo');
console.log(demo);
json
{
  "exports": {
    "import": "./dist/esm/index.js",
    "require": "./dist/cjs/index.js"
  }
}

上述例子中,依赖方 main-esm.jsmain-cjs.js 分别通过 importrequire 加载 demo 包,node 会根据 demo/package.json 中的 exports 字段来决定 demo 包的入口模块路径。但这仅意味着是 npm 包的入口模块路径,换句话说,node 并不会因为 npm 包的入口模块是由 exports 字段的 importrequire 决定而将入口模块认定为相对应模块格式进行解析。

npm 包的入口模块路径确认后,node 会做如下决策:

  1. 优先看文件扩展名:若以 .mjs 结尾,则以 esm 模块解析,若以 .cjs 结尾,则以 cjs 模块解析。

  2. 对于 .js 文件(或无扩展名文件):查找距离这个文件最近的 package.json 中的 type 字段(向上冒泡查找)。

    • "type": "module" 表示作用域内的 .js 文件默认为 esm 模块。
    • "type": "commonjs" (或无 "type" 字段) 表示作用域内的 .js 文件默认为 cjs 模块。
  3. 如果无法通过以上方式确定,对于 .js 文件,node 可能会尝试通过 DETECT_MODULE_SYNTAX 进行语法检测。

    DETECT_MODULE_SYNTAX

    这是 node 的启发式做法,尝试将源码按 esm 规范解析。

    若模块源码可以以 esm 规范解析成功,那么 node 会按照下述策略进一步进行解析:

    • 若源码中包含了 esm 特有的语法特性(静态 importexport 语句、import.metaTLA),那么则认为是 esm 模块。
    • commonjs 中具有特殊的 预定义变量,例如 requireexportsmodule__filename__dirname,这些 预定义变量esm 规范中并没有特定的意义。因此模块中若以 constletclass 关键字声明了上述标识符,那么 node 会认为这个模块不是 commonjs 模块,会以 esm 模块解析。

    若模块源码无法以 esm 规范解析成功,那么 node 会认为该模块为 commonjs 模块。

  4. 若无法通过 DETECT_MODULE_SYNTAX 进行语法检测,那么 node 会认为该模块为 commonjs 模块(兼容性考量)。

回到上述例子的 main-cjs.js 中模块中,node 通过包解析算法发现 demo 包的入口模块路径为 ./dist/cjs/index.js./dist/cjs/index.js 是以 .js 为后缀的文件,那么 node 会向上冒泡查找 package.json 中的 type 字段。若 package.json 中存在 type 字段,那么 node 会根据 type 字段的值来决定解析模块的方式,若 type 字段不存在,那么 node 会尝试通过 DETECT_MODULE_SYNTAX 进行语法检测。最后兜底认为该模块为 commonjs 模块。

因此 require('demo') 可能是 require(commonjs)require(esm) 的产物,取决于上述 node 的包解析算法。同理 import demo from 'demo' 可能是 import demo from commonjsimport demo from esm 的产物。

node 解析模块的行为并不是由 package.json 中的 exports 字段决定的,而是由包作者决定的。

贡献者

页面历史

Discuss

根据 CC BY-SA 4.0 许可证发布。 (9037680)