模块:包#

简介#

包是一个由 package.json 文件描述的文件夹树。包由包含 package.json 文件的文件夹及其所有子文件夹组成,直到遇到另一个包含 package.json 的文件夹或名为 node_modules 的文件夹为止。

本页为编写 package.json 文件的包作者提供指导,并作为 Node.js 定义的 package.json 字段的参考。

确定模块系统#

简介#

当作为初始输入传递给 node,或者被 import 语句或 import() 表达式引用时,Node.js 会将以下内容视为 ES 模块

  • 扩展名为 .mjs 的文件。

  • 当最近的父级 package.json 文件包含值为 "module" 的顶层 "type" 字段时,扩展名为 .js 的文件。

  • 在使用 --input-type=module 标志的情况下,作为 --eval 的参数传递或通过 STDIN 管道传输给 node 的字符串。

  • 仅能成功解析为 ES 模块 的代码语法(如 importexport 语句或 import.meta),且没有关于应如何解释它的显式标记。显式标记包括 .mjs.cjs 扩展名、值为 "module""commonjs"package.json "type" 字段,或 --input-type 标志。动态 import() 表达式在 CommonJS 或 ES 模块中均受支持,并且不会强制将文件视为 ES 模块。参见 语法检测

当作为初始输入传递给 node,或者被 import 语句或 import() 表达式引用时,Node.js 会将以下内容视为 CommonJS

  • 扩展名为 .cjs 的文件。

  • 当最近的父级 package.json 文件包含值为 "commonjs" 的顶层 "type" 字段时,扩展名为 .js 的文件。

  • 在使用 --input-type=commonjs 标志的情况下,作为 --eval--print 的参数传递或通过 STDIN 管道传输给 node 的字符串。

  • 没有父级 package.json 文件或最近的父级 package.json 缺乏 type 字段的 .js 文件,且该代码可成功评估为 CommonJS。换句话说,Node.js 首先尝试将此类“歧义”文件作为 CommonJS 运行;如果因解析器发现 ES 模块语法导致 CommonJS 评估失败,它将重试将其作为 ES 模块进行评估。

在“歧义”文件中编写 ES 模块语法会带来性能成本,因此鼓励作者尽可能显式。特别是,包作者应始终在 package.json 文件中包含 "type" 字段,即使在所有源码均为 CommonJS 的包中也是如此。明确包的 type 可以使包在 Node.js 的默认类型发生变化时具备前瞻性,同时也使构建工具和加载器更容易确定应如何解释包中的文件。

语法检测#

稳定性:1.2 - 候选发布

Node.js 将检查歧义输入的源代码以确定其是否包含 ES 模块语法;如果检测到此类语法,输入将被视为 ES 模块。

歧义输入的定义如下:

  • 扩展名为 .js 或无扩展名的文件;且没有控制性的 package.json 文件或缺乏 type 字段的文件。
  • 未指定 --input-type 时的字符串输入(--evalSTDIN)。

ES 模块语法定义为在作为 CommonJS 评估时会抛出的语法。这包括以下内容:

  • import 语句(但不包括 import() 表达式,后者在 CommonJS 中是合法的)。
  • export 语句。
  • import.meta 引用。
  • 模块顶层的 await
  • CommonJS 包装器变量(require, module, exports, __dirname, __filename)的词法重新声明。

模块解析与加载#

Node.js 有两种类型的模块解析和加载方式,根据请求模块的方式进行选择。

当通过 require() 请求模块时(CommonJS 模块中默认提供,且可在 CommonJS 和 ES 模块中使用 createRequire() 动态生成):

  • 解析
    • require() 发起的解析支持文件夹作为模块
    • 在解析说明符时,如果没有找到精确匹配,require() 将尝试添加扩展名(.js.json,最后是 .node),然后尝试解析文件夹作为模块
    • 默认不支持以 URL 作为说明符。
  • 加载
    • .json 文件被视为 JSON 文本文件。
    • .node 文件被解释为使用 process.dlopen() 加载的已编译插件模块。
    • .ts.mts.cts 文件被视为 TypeScript 文本文件。
    • 具有任何其他扩展名或没有扩展名的文件被视为 JavaScript 文本文件。
    • 只有当 ECMAScript 模块 及其依赖项 是同步的(即它们不包含顶层 await)时,require() 才能用于 从 CommonJS 模块加载 ECMAScript 模块

当通过静态 import 语句(仅在 ES 模块中可用)或 import() 表达式(在 CommonJS 和 ES 模块中均可用)请求模块时:

  • 解析
    • import/import() 的解析不支持文件夹作为模块,必须完全指定目录索引(例如 './startup/index.js')。
    • 它不执行扩展名搜索。当说明符是相对或绝对文件 URL 时,必须提供文件扩展名。
    • 默认支持 file://data: URL 作为说明符。
  • 加载
    • .json 文件被视为 JSON 文本文件。导入 JSON 模块时,需要导入类型属性(例如 import json from './data.json' with { type: 'json' })。
    • 如果启用了 --experimental-addon-modules.node 文件将被解释为使用 process.dlopen() 加载的已编译插件模块。
    • .ts.mts.cts 文件被视为 TypeScript 文本文件。
    • 对于 JavaScript 文本文件,仅接受 .js.mjs.cjs 扩展名。
    • .wasm 文件被视为 WebAssembly 模块
    • 任何其他文件扩展名将导致 ERR_UNKNOWN_FILE_EXTENSION 错误。可以通过 自定义钩子 支持其他文件扩展名。
    • import/import() 可用于加载 JavaScript CommonJS 模块。此类模块通过 merve 进行处理,以尝试识别命名导出,如果可以通过静态分析确定,则可以使用这些导出。

无论模块如何被请求,解析和加载过程都可以使用 自定义钩子 进行定制。

package.json 与文件扩展名#

在包内,package.json"type" 字段定义了 Node.js 应如何解释 .js 文件。如果 package.json 文件没有 "type" 字段,.js 文件将被视为 CommonJS

package.json"type" 值为 "module",告诉 Node.js 将该包内的 .js 文件解释为使用 ES 模块 语法。

"type" 字段不仅适用于初始入口点(node my-app.js),也适用于由 import 语句和 import() 表达式引用的文件。

// my-app.js, treated as an ES module because there is a package.json
// file in the same folder with "type": "module".

import './startup/init.js';
// Loaded as ES module since ./startup contains no package.json file,
// and therefore inherits the "type" value from one level up.

import 'commonjs-package';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs".

import './node_modules/commonjs-package/index.js';
// Loaded as CommonJS since ./node_modules/commonjs-package/package.json
// lacks a "type" field or contains "type": "commonjs".

无论最近的父级 package.json 如何,以 .mjs 结尾的文件始终作为 ES 模块 加载。

无论最近的父级 package.json 如何,以 .cjs 结尾的文件始终作为 CommonJS 加载。

import './legacy-file.cjs';
// Loaded as CommonJS since .cjs is always loaded as CommonJS.

import 'commonjs-package/src/index.mjs';
// Loaded as ES module since .mjs is always loaded as ES module.

.mjs.cjs 扩展名可用于在同一个包内混合类型:

  • "type": "module" 包内,通过使用 .cjs 扩展名命名特定文件,可以指示 Node.js 将其解释为 CommonJS(因为在 "module" 包内,.js.mjs 文件都被视为 ES 模块)。

  • "type": "commonjs" 包内,通过使用 .mjs 扩展名命名特定文件,可以指示 Node.js 将其解释为 ES 模块(因为在 "commonjs" 包内,.js.cjs 文件都被视为 CommonJS)。

--input-type 标志#

当设置 --input-type=module 标志时,作为 --eval(或 -e)的参数传递或通过 STDIN 管道传输给 node 的字符串将被视为 ES 模块

node --input-type=module --eval "import { sep } from 'node:path'; console.log(sep);"

echo "import { sep } from 'node:path'; console.log(sep);" | node --input-type=module

为了完整起见,还有一个 --input-type=commonjs,用于显式地将字符串输入作为 CommonJS 运行。如果未指定 --input-type,这是默认行为。

包入口点#

在包的 package.json 文件中,两个字段可以定义包的入口点:"main""exports"。这两个字段都适用于 ES 模块和 CommonJS 模块的入口点。

"main" 字段在所有 Node.js 版本中均受支持,但其功能有限:它仅定义包的主入口点。

"exports" 提供了 "main" 的现代替代方案,允许定义多个入口点、支持环境间的条件入口解析,并防止除 "exports" 中定义的那些入口点之外的任何其他入口点。这种封装允许模块作者清楚地定义其包的公共接口。

对于针对当前支持的 Node.js 版本的新包,推荐使用 "exports" 字段。对于支持 Node.js 10 及以下版本的包,需要 "main" 字段。如果同时定义了 "exports""main",在支持的 Node.js 版本中,"exports" 字段优先于 "main"

条件导出 可在 "exports" 中使用,以根据环境定义不同的包入口点,包括包是通过 require 还是通过 import 引用。有关在单个包中支持 CommonJS 和 ES 模块的更多信息,请查阅 双重 CommonJS/ES 模块包部分

引入 "exports" 字段的现有包将阻止包的消费者使用任何未定义的入口点,包括 package.json(例如 require('your-package/package.json'))。这很可能是一个破坏性变更。

为了使 "exports" 的引入不产生破坏性,请确保导出每个先前支持的入口点。最好显式指定入口点,以便包的公共 API 定义明确。例如,一个以前导出 mainlibfeaturepackage.json 的项目可以使用以下 package.exports

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/index": "./lib/index.js",
    "./lib/index.js": "./lib/index.js",
    "./feature": "./feature/index.js",
    "./feature/index": "./feature/index.js",
    "./feature/index.js": "./feature/index.js",
    "./package.json": "./package.json"
  }
}

或者,项目可以选择使用导出模式同时导出整个文件夹(包括带扩展名和不带扩展名的子路径)。

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./lib": "./lib/index.js",
    "./lib/*": "./lib/*.js",
    "./lib/*.js": "./lib/*.js",
    "./feature": "./feature/index.js",
    "./feature/*": "./feature/*.js",
    "./feature/*.js": "./feature/*.js",
    "./package.json": "./package.json"
  }
}

通过上述方式为任何次要版本的包提供向后兼容性,包的未来主要版本变更可以适当地将导出限制为仅公开特定的功能导出。

{
  "name": "my-package",
  "exports": {
    ".": "./lib/index.js",
    "./feature/*.js": "./feature/*.js",
    "./feature/internal/*": null
  }
}

主入口点导出#

编写新包时,建议使用 "exports" 字段:

{
  "exports": "./index.js"
}

当定义了 "exports" 字段时,包的所有子路径都被封装,不再对导入者可用。例如,require('pkg/subpath.js') 会抛出 ERR_PACKAGE_PATH_NOT_EXPORTED 错误。

这种导出封装为工具和处理包的语义化版本(semver)升级时提供了更可靠的包接口保证。这不是强封装,因为对包的任何绝对子路径的直接 require,例如 require('/path/to/node_modules/pkg/subpath.js'),仍然会加载 subpath.js

所有当前受支持的 Node.js 版本和现代构建工具都支持 "exports" 字段。对于使用旧版本 Node.js 或相关构建工具的项目,可以通过在 "exports" 旁边包含指向同一模块的 "main" 字段来实现兼容性:

{
  "main": "./index.js",
  "exports": "./index.js"
}

子路径导出#

使用 "exports" 字段时,可以通过将主入口点视为 "." 子路径来定义自定义子路径:

{
  "exports": {
    ".": "./index.js",
    "./submodule.js": "./src/submodule.js"
  }
}

现在,消费者只能导入 "exports" 中定义的子路径。

import submodule from 'es-module-package/submodule.js';
// Loads ./node_modules/es-module-package/src/submodule.js

而其他子路径将报错。

import submodule from 'es-module-package/private-module.js';
// Throws ERR_PACKAGE_PATH_NOT_EXPORTED
子路径中的扩展名#

包作者应在导出中提供带扩展名(import 'pkg/subpath.js')或无扩展名(import 'pkg/subpath')的子路径。这确保了每个导出的模块只有一个子路径,以便所有依赖项都导入相同的、一致的说明符,从而保持包契约对消费者清晰,并简化包子路径的补全。

传统上,包倾向于使用无扩展名风格,这具有可读性和掩盖文件中包内真实路径的好处。

随着 导入映射(import maps) 现在为浏览器和其他 JavaScript 运行环境中的包解析提供了标准,使用无扩展名风格可能导致导入映射定义臃肿。显式文件扩展名可以通过使导入映射能够利用 包文件夹映射 来避免此问题,从而在可能的情况下映射多个子路径,而不是为每个包子路径导出建立单独的映射条目。这也反映了在相对和绝对导入说明符中使用 完整说明符路径 的要求。

导出目标的路径规则与验证#

"exports" 字段中定义路径作为目标时,Node.js 会强制执行若干规则以确保安全性、可预测性和适当的封装。了解这些规则对于发布包的作者至关重要。

目标必须是相对 URL#

"exports" 映射中的所有目标路径(导出键关联的值)必须是以 ./ 开头的相对 URL 字符串。

// package.json
{
  "name": "my-package",
  "exports": {
    ".": "./dist/main.js",          // Correct
    "./feature": "./lib/feature.js", // Correct
    // "./origin-relative": "/dist/main.js", // Incorrect: Must start with ./
    // "./absolute": "file:///dev/null", // Incorrect: Must start with ./
    // "./outside": "../common/util.js" // Incorrect: Must start with ./
  }
}

此行为的原因包括:

  • 安全性: 防止从包自身目录之外导出任意文件。
  • 封装: 确保所有导出路径都相对于包根目录进行解析,使包自包含。
禁止路径遍历或非法段#

导出目标不得解析到包根目录之外的位置。此外,在初始 ./ 之后以及替换到目标模式中的任何 subpath 部分中,通常禁止使用 .(单点)、..(双点)或 node_modules(及其 URL 编码的等效项)等路径段。

// package.json
{
  "name": "my-package",
  "exports": {
    // ".": "./dist/../../elsewhere/file.js", // Invalid: path traversal
    // ".": "././dist/main.js",             // Invalid: contains "." segment
    // ".": "./dist/../dist/main.js",       // Invalid: contains ".." segment
    // "./utils/./helper.js": "./utils/helper.js" // Key has invalid segment
  }
}

导出语法糖#

如果 "." 导出是唯一的导出,则 "exports" 字段提供了一种语法糖,即直接作为 "exports" 字段值:

{
  "exports": {
    ".": "./index.js"
  }
}

可以写成:

{
  "exports": "./index.js"
}

子路径导入#

除了 "exports" 字段外,还有一个包 "imports" 字段,用于创建仅适用于包自身内部导入说明符的私有映射。

"imports" 字段中的条目必须始终以 # 开头,以确保它们与外部包说明符区分开来。

例如,imports 字段可用于为内部模块获得条件导出的好处:

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
}

其中 import '#dep' 不会获得外部包 dep-node-native 的解析(包括其自身的导出),而是获得相对于包的本地文件 ./dep-polyfill.js(在其他环境中)。

"exports" 字段不同,"imports" 字段允许映射到外部包。

imports 字段的解析规则在其他方面类似于 exports 字段。

子路径模式#

对于导出或导入较少的包,我们建议显式列出每个导出子路径条目。但对于具有大量子路径的包,这可能会导致 package.json 臃肿和维护问题。

对于这些用例,可以使用子路径导出模式:

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js"
  },
  "imports": {
    "#internal/*.js": "./src/internal/*.js"
  }
}

* 映射公开嵌套子路径,因为它仅是字符串替换语法。

右侧的所有 * 实例随后都将被此值替换,即使它包含任何 / 分隔符。

import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js

import featureY from 'es-module-package/features/y/y.js';
// Loads ./node_modules/es-module-package/src/features/y/y.js

import internalZ from '#internal/z.js';
// Loads ./src/internal/z.js

这是直接的静态匹配和替换,没有任何针对文件扩展名的特殊处理。在映射的两侧包含 "*.js" 限制了公开的包导出仅为 JS 文件。

导出静态可枚举的属性通过导出模式得以维护,因为包的单个导出可以通过将右侧目标模式视为针对包内文件列表的 ** 全局匹配来确定。由于 node_modules 路径在导出目标中是被禁止的,因此这种扩展仅取决于包自身的文件。

要从模式中排除私有子文件夹,可以使用 null 目标:

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/*.js": "./src/features/*.js",
    "./features/private-internal/*": null
  }
}
import featureInternal from 'es-module-package/features/private-internal/m.js';
// Throws: ERR_PACKAGE_PATH_NOT_EXPORTED

import featureX from 'es-module-package/features/x.js';
// Loads ./node_modules/es-module-package/src/features/x.js

条件导出#

条件导出提供了一种根据特定条件映射到不同路径的方法。它们同时支持 CommonJS 和 ES 模块导入。

例如,想要为 require()import 提供不同 ES 模块导出的包可以这样写:

// package.json
{
  "exports": {
    "import": "./index-module.js",
    "require": "./index-require.cjs"
  },
  "type": "module"
}

Node.js 实现了以下条件,按从最具体到最不具体的顺序排列(条件应按此顺序定义):

  • "node-addons" - 类似于 "node",匹配任何 Node.js 环境。此条件可用于提供使用原生 C++ 插件的入口点,而不是使用更通用且不依赖于原生插件的入口点。此条件可通过 --no-addons 标志 禁用。
  • "node" - 匹配任何 Node.js 环境。可以是 CommonJS 或 ES 模块文件。在大多数情况下,无需明确指明 Node.js 平台。
  • "import" - 当包通过 importimport() 或通过 ECMAScript 模块加载器的任何顶层导入或解析操作加载时匹配。无论目标文件的模块格式如何,此条件均适用。始终与 "require" 互斥。
  • "require" - 当包通过 require() 加载时匹配。所引用的文件应可使用 require() 加载,尽管无论目标文件的模块格式如何,该条件均匹配。预期的格式包括 CommonJS、JSON、原生插件和 ES 模块。始终与 "import" 互斥。
  • "module-sync" - 无论包是通过 importimport() 还是 require() 加载,该条件均匹配。格式预期为在其模块图中不包含顶层 await 的 ES 模块——如果包含,则在 require() 该模块时将抛出 ERR_REQUIRE_ASYNC_MODULE
  • "default" - 始终匹配的通用回退。可以是 CommonJS 或 ES 模块文件。此条件应始终放在最后。

"exports" 对象内,键顺序非常重要。在条件匹配期间,较早的条目具有更高的优先级,优于后面的条目。通用规则是条件应按对象顺序从最具体到最不具体排列。

使用 "import""require" 条件可能会导致一些隐患,这在 双重 CommonJS/ES 模块包部分 中有进一步解释。

"node-addons" 条件可用于提供使用原生 C++ 插件的入口点。但是,此条件可通过 --no-addons 标志 禁用。使用 "node-addons" 时,建议将 "default" 视为增强功能,提供更通用的入口点,例如使用 WebAssembly 而非原生插件。

条件导出也可以扩展到导出子路径,例如:

{
  "exports": {
    ".": "./index.js",
    "./feature.js": {
      "node": "./feature-node.js",
      "default": "./feature.js"
    }
  }
}

定义了一个包,其中 require('pkg/feature.js')import 'pkg/feature.js' 可以在 Node.js 和其他 JS 环境之间提供不同的实现。

使用环境分支时,尽可能始终包含 "default" 条件。提供 "default" 条件可确保任何未知的 JS 环境都能使用此通用实现,这有助于避免这些 JS 环境为了支持带有条件导出的包而不得不伪装成现有环境。因此,使用 "node""default" 条件分支通常优于使用 "node""browser" 条件分支。

嵌套条件#

除了直接映射外,Node.js 还支持嵌套条件对象。

例如,定义一个仅在 Node.js 中有双模式入口点而不在浏览器中使用的包:

{
  "exports": {
    "node": {
      "import": "./feature-node.mjs",
      "require": "./feature-node.cjs"
    },
    "default": "./feature.mjs"
  }
}

条件会像扁平条件一样按顺序匹配。如果嵌套条件没有任何映射,它将继续检查父条件中的其余条件。以这种方式,嵌套条件的行为类似于嵌套的 JavaScript if 语句。

解析用户条件#

运行 Node.js 时,可以使用 --conditions 标志添加自定义用户条件:

node --conditions=development index.js

这将解析包导入和导出中的 "development" 条件,同时适当地解析现有的 "node""node-addons""default""import""require" 条件。

可以通过重复标志设置任意数量的自定义条件。

典型条件应仅包含字母数字字符,必要时使用 ":"、"-" 或 "=" 作为分隔符。任何其他字符可能会在 Node.js 之外遇到兼容性问题。

在 Node.js 中,条件的限制很少,但具体包括:

  1. 它们必须包含至少一个字符。
  2. 它们不能以 "." 开头,因为它们可能出现在也允许相对路径的地方。
  3. 它们不能包含 ",",因为某些 CLI 工具可能会将其解析为逗号分隔的列表。
  4. 它们不能是像 "10" 这样的整数属性键,因为这可能对 JS 对象的属性键顺序产生意外影响。

社区条件定义#

Node.js 内核实现"import""require""node""module-sync""node-addons""default" 条件外,其他条件字符串默认被忽略。

其他平台可能会实现其他条件,用户条件可以通过 --conditions / -C 标志 在 Node.js 中启用。

由于自定义包条件需要清晰的定义以确保正确使用,下方提供了一份常见的已知包条件列表及其严格定义,以协助生态系统协调。

  • "types" - 可供类型系统用于解析给定导出的类型文件。此条件应始终放在第一位。
  • "browser" - 任何 Web 浏览器环境。
  • "development" - 可用于定义仅开发环境的入口点,例如提供额外的调试上下文,如在开发模式下运行时提供更好的错误消息。必须始终与 "production" 互斥。
  • "production" - 可用于定义生产环境的入口点。必须始终与 "development" 互斥。

对于其他运行时,平台特定的条件键定义由 WinterCGRuntime Keys 提案规范中维护。

可以通过向 Node.js 文档的此部分 创建拉取请求,将新的条件定义添加到此列表中。在此处列出新条件定义的要求是:

  • 定义对所有实现者应清晰且无歧义。
  • 为何需要该条件的用例应有明确的论证。
  • 应存在足够的现有实现使用。
  • 条件名称不应与另一个条件定义或广泛使用的条件冲突。
  • 列出该条件定义应为生态系统提供协调利益,否则无法实现。例如,这对于特定于公司或特定于应用程序的条件不一定适用。
  • 该条件应使 Node.js 用户预期它出现在 Node.js 内核文档中。"types" 条件是一个很好的例子:它并不真正属于 Runtime Keys 提案,但非常适合在 Node.js 文档中列出。

上述定义可能会在适当的时候移动到专门的条件注册表中。

使用包名自引用#

在包内,包 package.json "exports" 字段中定义的值可以通过包名进行引用。例如,假设 package.json 为:

// package.json
{
  "name": "a-package",
  "exports": {
    ".": "./index.mjs",
    "./foo.js": "./foo.js"
  }
}

那么该包内的任何模块都可以引用包自身的导出:

// ./a-module.mjs
import { something } from 'a-package'; // Imports "something" from ./index.mjs.

仅当 package.json 具有 "exports" 时,自引用才可用,并且仅允许导入该 "exports"(在 package.json 中)允许的内容。因此,对于之前的包,以下代码将产生运行时错误:

// ./another-module.mjs

// Imports "another" from ./m.mjs. Fails because
// the "package.json" "exports" field
// does not provide an export named "./m.mjs".
import { another } from 'a-package/m.mjs';

在使用 require 时,无论是在 ES 模块中还是在 CommonJS 模块中,自引用也可用。例如,此代码也将工作:

// ./a-module.js
const { something } = require('a-package/foo.js'); // Loads from ./foo.js.

最后,自引用也适用于作用域包。例如,此代码也将工作:

// package.json
{
  "name": "@my/package",
  "exports": "./index.js"
}
// ./index.js
module.exports = 42;
// ./other.js
console.log(require('@my/package'));
$ node other.js
42

双重 CommonJS/ES 模块包#

详细信息请参见 包示例仓库

Node.js package.json 字段定义#

本节描述 Node.js 运行时使用的字段。其他工具(如 npm)使用其他字段,这些字段被 Node.js 忽略且此处未记录。

package.json 文件中的以下字段在 Node.js 中使用:

  • "name" - 在包内使用命名导入时相关。也被包管理器用作包名。
  • "main" - 加载包时的默认模块(如果未指定 exports),以及在引入 exports 之前的 Node.js 版本中。
  • "type" - 包类型,决定是将 .js 文件加载为 CommonJS 还是 ES 模块。
  • "exports" - 包导出和条件导出。存在时,限制可从包内加载哪些子模块。
  • "imports" - 包导入,供包自身的模块使用。

"name"#

{
  "name": "package-name"
}

"name" 字段定义您的包名。发布到 npm 注册表需要一个满足 特定要求 的名称。

"name" 字段可以与 "exports" 字段结合使用,以使用其包名 自引用 包。

"main"#

{
  "main": "./index.js"
}

"main" 字段定义了通过 node_modules 查找按名称导入时的包入口点。其值为路径。

如果存在 "exports" 字段,则按名称导入包时,它优先于 "main" 字段。

它还定义了当 通过 require() 加载包目录 时所使用的脚本。

// This resolves to ./path/to/directory/index.js.
require('./path/to/directory');

"type"#

"type" 字段定义了 Node.js 对所有以该 package.json 文件作为最近父级的文件所使用的模块格式。

当最近的父级 package.json 文件包含值为 "module" 的顶层 "type" 字段时,以 .js 结尾的文件被加载为 ES 模块。

最近的父级 package.json 定义为在当前文件夹、该文件夹的父文件夹等路径中搜索时找到的第一个 package.json,直到达到 node_modules 文件夹或卷根目录为止。

// package.json
{
  "type": "module"
}
# In same folder as preceding package.json
node my-app.js # Runs as ES module

如果最近的父级 package.json 缺乏 "type" 字段,或包含 "type": "commonjs",则 .js 文件被视为 CommonJS。如果到达卷根目录且未找到 package.json,则 .js 文件被视为 CommonJS

如果最近的父级 package.json 包含 "type": "module",则 .js 文件的 import 语句被视为 ES 模块。

// my-app.js, part of the same example as above
import './startup.js'; // Loaded as ES module because of package.json

无论 "type" 字段的值如何,.mjs 文件始终被视为 ES 模块,.cjs 文件始终被视为 CommonJS。

"exports"#

{
  "exports": "./index.js"
}

"exports" 字段允许在通过 node_modules 查找或通过对其自身名称的 自引用 加载时,定义按名称导入的包的 入口点。它在 Node.js 12+ 中受支持,作为 "main" 的替代方案,可以支持定义 子路径导出条件导出,同时封装内部未导出的模块。

条件导出 也可在 "exports" 内使用,以定义每个环境的不同包入口点,包括包是通过 require 还是通过 import 引用。

"exports" 中定义的所有路径必须是以 ./ 开头的相对文件 URL。

"imports"#

// package.json
{
  "imports": {
    "#dep": {
      "node": "dep-node-native",
      "default": "./dep-polyfill.js"
    }
  },
  "dependencies": {
    "dep-node-native": "^1.0.0"
  }
}

imports 字段中的条目必须是以 # 开头的字符串。

包导入允许映射到外部包。

此字段定义当前包的 子路径导入