模块:ECMAScript 模块#

稳定性:2 - 稳定

简介#

ECMAScript 模块是打包 JavaScript 代码以供重用的官方标准格式。模块使用各种 importexport 语句定义。

以下 ES 模块示例导出一个函数:

// addTwo.mjs
function addTwo(num) {
  return num + 2;
}

export { addTwo }; 

以下 ES 模块示例从 addTwo.mjs 导入函数:

// app.mjs
import { addTwo } from './addTwo.mjs';

// Prints: 6
console.log(addTwo(4)); 

Node.js 完全支持当前指定的 ECMAScript 模块,并提供它们与其原始模块格式 CommonJS 之间的互操作性。

启用#

Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块。

开发者可以通过 .mjs 文件扩展名、package.json"type" 字段值为 "module",或 --input-type 标志值为 "module" 来告诉 Node.js 将 JavaScript 解释为 ES 模块。这些都是代码旨在作为 ES 模块运行的明确标记。

相反,开发者也可以通过 .cjs 文件扩展名、package.json"type" 字段值为 "commonjs",或 --input-type 标志值为 "commonjs" 来明确告诉 Node.js 将 JavaScript 解释为 CommonJS。

当代码缺少任何一种模块系统的明确标记时,Node.js 将检查模块的源代码以寻找 ES 模块语法。如果找到此类语法,Node.js 将作为 ES 模块运行代码;否则它将作为 CommonJS 运行模块。更多详情请参阅确定模块系统

#

此部分已移至模块:包

import 说明符#

术语#

import 语句的*说明符*是 from 关键字后面的字符串,例如 import { sep } from 'node:path' 中的 'node:path'。说明符也用于 export from 语句,以及作为 import() 表达式的参数。

说明符有三种类型:

  • 相对说明符,如 './startup.js''../config.mjs'。它们指向相对于导入文件位置的路径。对于这些说明符,文件扩展名总是必需的。

  • 裸说明符,如 'some-package''some-package/shuffle'。它们可以按包名引用包的主入口点,或按示例所示,以包名为前缀引用包内的特定功能模块。只有在包没有 "exports" 字段时才需要包含文件扩展名。

  • 绝对说明符,如 'file:///opt/nodejs/config.js'。它们直接且明确地指向一个完整路径。

裸说明符的解析由Node.js 模块解析和加载算法处理。所有其他说明符的解析始终仅使用标准的相对 URL 解析语义。

与 CommonJS 中一样,可以通过在包名后附加路径来访问包内的模块文件,除非包的 package.json 包含 "exports" 字段,在这种情况下,只能通过 "exports" 中定义的路径来访问包内文件。

有关应用于 Node.js 模块解析中裸说明符的这些包解析规则的详细信息,请参阅包文档

强制文件扩展名#

当使用 import 关键字解析相对或绝对说明符时,必须提供文件扩展名。目录索引(例如 './startup/index.js')也必须完整指定。

此行为与 import 在浏览器环境中的行为一致,假设服务器配置典型。

URL#

ES 模块以 URL 的形式进行解析和缓存。这意味着特殊字符必须进行百分号编码,例如 # 编码为 %23? 编码为 %3F

支持 file:node:data: URL 方案。像 'https://example.com/app.js' 这样的说明符在 Node.js 中本身不受支持,除非使用自定义 HTTPS 加载器

file: URL#

如果用于解析模块的 import 说明符具有不同的查询或片段,模块将被多次加载。

import './foo.mjs?query=1'; // loads ./foo.mjs with query of "?query=1"
import './foo.mjs?query=2'; // loads ./foo.mjs with query of "?query=2" 

卷根可以通过 ///file:/// 引用。鉴于 URL 和路径解析之间的差异(例如百分号编码细节),建议在导入路径时使用 url.pathToFileURL

data: 导入#

支持使用data: URL导入以下 MIME 类型:

  • text/javascript 用于 ES 模块
  • application/json 用于 JSON
  • application/wasm 用于 Wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' with { type: 'json' }; 

data: URL 只解析内置模块的裸说明符绝对说明符。解析相对说明符不起作用,因为 data: 不是一个特殊方案。例如,尝试从 data:text/javascript,import "./foo"; 加载 ./foo 会因无法解析而失败,因为 data: URL 没有相对解析的概念。

node: 导入#

支持 node: URL 作为加载 Node.js 内置模块的替代方法。此 URL 方案允许通过有效的绝对 URL 字符串引用内置模块。

import fs from 'node:fs/promises'; 

导入属性#

导入属性是模块导入语句的一种内联语法,用于在模块说明符旁边传递更多信息。

import fooData from './foo.json' with { type: 'json' };

const { default: barData } =
  await import('./bar.json', { with: { type: 'json' } }); 

Node.js 只支持 type 属性,并支持以下值:

属性 type需要用于
'json'JSON 模块

导入 JSON 模块时,type: 'json' 属性是必需的。

内置模块#

内置模块提供其公共 API 的命名导出。还提供了一个默认导出,其值为 CommonJS 的 exports。默认导出可用于修改命名导出等操作。内置模块的命名导出仅通过调用 module.syncBuiltinESMExports() 进行更新。

import EventEmitter from 'node:events';
const e = new EventEmitter(); 
import { readFile } from 'node:fs';
readFile('./foo.txt', (err, source) => {
  if (err) {
    console.error(err);
  } else {
    console.log(source);
  }
}); 
import fs, { readFileSync } from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { Buffer } from 'node:buffer';

fs.readFileSync = () => Buffer.from('Hello, ESM');
syncBuiltinESMExports();

fs.readFileSync === readFileSync; 

import() 表达式#

动态 import() 提供了一种异步导入模块的方式。它在 CommonJS 和 ES 模块中都受支持,并可用于加载 CommonJS 和 ES 模块。

import.meta#

import.meta 元属性是一个包含以下属性的 Object。它仅在 ES 模块中受支持。

import.meta.dirname#

  • 类型:<string> 当前模块的目录名。

这与 import.meta.filenamepath.dirname() 相同。

注意:仅存在于 file: 模块中。

import.meta.filename#

  • 类型:<string> 当前模块的完整绝对路径和文件名,符号链接已解析。

这与 import.meta.urlurl.fileURLToPath() 相同。

注意:只有本地模块支持此属性。不使用 file: 协议的模块将不提供此属性。

import.meta.url#

  • 类型:<string> 模块的绝对 file: URL。

它的定义与浏览器中完全相同,提供当前模块文件的 URL。

这使得一些有用的模式成为可能,例如相对文件加载:

import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url)); 

import.meta.main#

稳定性:1.0 - 早期开发

  • 类型:<boolean> 当当前模块是当前进程的入口点时为 true;否则为 false

等同于 CommonJS 中的 require.main === module

类似于 Python 的 __name__ == "__main__"

export function foo() {
  return 'Hello, world';
}

function main() {
  const message = foo();
  console.log(message);
}

if (import.meta.main) main();
// `foo` can be imported from another module without possible side-effects from `main` 

import.meta.resolve(specifier)#

稳定性:1.2 - 候选发布

  • specifier <string> 相对于当前模块要解析的模块说明符。
  • 返回:<string> 该说明符将解析为的绝对 URL 字符串。

import.meta.resolve 是一个作用于每个模块的、相对于模块的解析函数,返回 URL 字符串。

const dependencyAsset = import.meta.resolve('component-lib/asset.css');
// file:///app/node_modules/component-lib/asset.css
import.meta.resolve('./dep.js');
// file:///app/dep.js 

支持 Node.js 模块解析的所有功能。依赖项解析受包内允许的导出解析的限制。

注意事项:

  • 这可能导致同步的文件系统操作,这可能像 require.resolve 一样影响性能。
  • 此功能在自定义加载器中不可用(会导致死锁)。

非标准 API:

当使用 --experimental-import-meta-resolve 标志时,该函数接受第二个参数:

  • parent <string> | <URL> 一个可选的绝对父模块 URL,用于从中解析。默认值: import.meta.url

与 CommonJS 的互操作性#

import 语句#

一个 import 语句可以引用一个 ES 模块或一个 CommonJS 模块。import 语句只允许在 ES 模块中使用,但动态 import() 表达式在 CommonJS 中也受支持,用于加载 ES 模块。

当导入CommonJS 模块时,module.exports 对象作为默认导出提供。命名导出也可能可用,由静态分析提供,以方便更好地兼容生态系统。

require#

CommonJS 模块的 require 目前只支持加载同步的 ES 模块(即不使用顶层 await 的 ES 模块)。

详情请参阅使用 `require()` 加载 ECMAScript 模块

CommonJS 命名空间#

CommonJS 模块包含一个 module.exports 对象,它可以是任何类型。

为了支持这一点,当从 ECMAScript 模块导入 CommonJS 时,会为 CommonJS 模块构建一个命名空间包装器,该包装器始终提供一个指向 CommonJS module.exports 值的 default 导出键。

此外,还会对 CommonJS 模块的源文本进行启发式静态分析,以尽力获取一个静态的导出列表,以便从 module.exports 上的值提供给命名空间。这是必要的,因为这些命名空间必须在 CJS 模块评估之前构建。

这些 CommonJS 命名空间对象还提供 default 导出作为一个名为 'module.exports' 的导出,以便明确指出它们在 CommonJS 中的表示使用此值,而不是命名空间值。这反映了在 require(esm) 互操作支持中处理 'module.exports' 导出名称的语义。

当导入一个 CommonJS 模块时,可以使用 ES 模块的默认导入或其相应的语法糖来可靠地导入它:

import { default as cjs } from 'cjs';
// Identical to the above
import cjsSugar from 'cjs';

console.log(cjs);
console.log(cjs === cjsSugar);
// Prints:
//   <module.exports>
//   true 

这个模块命名空间奇异对象可以在使用 import * as m from 'cjs' 或动态导入时直接观察到:

import * as m from 'cjs';
console.log(m);
console.log(m === await import('cjs'));
// Prints:
//   [Module] { default: <module.exports>, 'module.exports': <module.exports> }
//   true 

为了更好地与 JS 生态系统中的现有用法兼容,Node.js 还尝试确定每个导入的 CommonJS 模块的 CommonJS 命名导出,并通过静态分析过程将它们作为单独的 ES 模块导出提供。

例如,考虑一个 CommonJS 模块,其内容如下:

// cjs.cjs
exports.name = 'exported'; 

上述模块支持在 ES 模块中进行命名导入:

import { name } from './cjs.cjs';
console.log(name);
// Prints: 'exported'

import cjs from './cjs.cjs';
console.log(cjs);
// Prints: { name: 'exported' }

import * as m from './cjs.cjs';
console.log(m);
// Prints:
//   [Module] {
//     default: { name: 'exported' },
//     'module.exports': { name: 'exported' },
//     name: 'exported'
//   } 

从最后一个被记录的模块命名空间奇异对象的例子可以看出,当模块被导入时,name 导出是从 module.exports 对象复制过来并直接设置在 ES 模块命名空间上的。

对于这些命名导出,不会检测到实时绑定的更新或添加到 module.exports 的新导出。

命名导出的检测基于常见的语法模式,但并不总能正确检测到命名导出。在这些情况下,使用上述的默认导入形式可能是更好的选择。

命名导出检测涵盖了许多常见的导出模式、再导出模式以及构建工具和转译器的输出。有关所实现的具体语义,请参阅 cjs-module-lexer

ES 模块与 CommonJS 之间的差异#

没有 requireexportsmodule.exports#

在大多数情况下,可以使用 ES 模块的 import 来加载 CommonJS 模块。

如果需要,可以在 ES 模块中使用 module.createRequire() 构建一个 require 函数。

没有 __filename__dirname#

这些 CommonJS 变量在 ES 模块中不可用。

__filename__dirname 的用例可以通过 import.meta.filenameimport.meta.dirname 来实现。

没有插件加载#

插件目前不支持 ES 模块导入。

它们可以改为使用 module.createRequire()process.dlopen 加载。

没有 require.main#

要替换 require.main === module,有 import.meta.main API。

没有 require.resolve#

相对路径解析可以通过 new URL('./local', import.meta.url) 来处理。

要完全替代 require.resolve,可以使用 import.meta.resolve API。

或者可以使用 module.createRequire()

没有 NODE_PATH#

NODE_PATH 不参与解析 import 说明符。如果需要此行为,请使用符号链接。

没有 require.extensions#

require.extensions 不被 import 使用。模块自定义钩子可以提供替代方案。

没有 require.cache#

require.cache 不被 import 使用,因为 ES 模块加载器有自己独立的缓存。

JSON 模块#

JSON 文件可以通过 import 引用:

import packageConfig from './package.json' with { type: 'json' }; 

with { type: 'json' } 语法是强制性的;请参阅导入属性

导入的 JSON 仅公开一个 default 导出。不支持命名导出。为了避免重复,会在 CommonJS 缓存中创建一个缓存条目。如果该 JSON 模块已经从相同路径导入过,CommonJS 会返回相同的对象。

Wasm 模块#

支持导入 WebAssembly 模块实例和 WebAssembly 源阶段导入。

这两种集成都符合WebAssembly 的 ES 模块集成提案

Wasm 源阶段导入#

稳定性:1.2 - 候选发布

源阶段导入提案允许使用 import source 关键字组合直接导入一个 WebAssembly.Module 对象,而不是获取一个已经实例化并带有其依赖项的模块实例。

当需要对 Wasm 进行自定义实例化,同时仍通过 ES 模块集成进行解析和加载时,这非常有用。

例如,要创建模块的多个实例,或将自定义导入传递给 library.wasm 的新实例:

import source libraryModule from './library.wasm';

const instance1 = await WebAssembly.instantiate(libraryModule, importObject1);

const instance2 = await WebAssembly.instantiate(libraryModule, importObject2); 

除了静态源阶段,还有一个通过 import.source 动态阶段导入语法的动态变体:

const dynamicLibrary = await import.source('./library.wasm');

const instance = await WebAssembly.instantiate(dynamicLibrary, importObject); 

JavaScript 字符串内置函数#

稳定性:1.2 - 候选发布

在导入 WebAssembly 模块时,WebAssembly JS 字符串内置函数提案通过 ESM 集成自动启用。这允许 WebAssembly 模块直接使用来自 wasm:js-string 命名空间的高效编译时字符串内置函数。

例如,以下 Wasm 模块使用 wasm:js-stringlength 内置函数导出一个字符串 getLength 函数:

(module
  ;; Compile-time import of the string length builtin.
  (import "wasm:js-string" "length" (func $string_length (param externref) (result i32)))

  ;; Define getLength, taking a JS value parameter assumed to be a string,
  ;; calling string length on it and returning the result.
  (func $getLength (param $str externref) (result i32)
    local.get $str
    call $string_length
  )

  ;; Export the getLength function.
  (export "getLength" (func $get_length))
) 
import { getLength } from './string-len.wasm';
getLength('foo'); // Returns 3. 

Wasm 内置函数是编译时导入,在模块编译期间链接,而不是在实例化期间。它们的行为不像普通的模块图导入,并且除非使用直接的 WebAssembly.compile API 重新编译模块并禁用字符串内置函数,否则无法通过 WebAssembly.Module.imports(mod) 检查或虚拟化。

在模块实例化之前以源阶段导入模块,也会自动使用编译时内置函数:

import source mod from './string-len.wasm';
const { exports: { getLength } } = await WebAssembly.instantiate(mod, {});
getLength('foo'); // Also returns 3. 

Wasm 实例阶段导入#

稳定性:1.1 - 活跃开发

实例导入允许任何 .wasm 文件作为普通模块导入,并支持它们反过来导入其他模块。

例如,一个包含以下内容的 index.js

import * as M from './library.wasm';
console.log(M); 

在以下条件下执行:

node index.mjs 

将为 library.wasm 的实例化提供导出接口。

保留的 Wasm 命名空间#

在导入 WebAssembly 模块实例时,它们不能使用以保留前缀开头的导入模块名或导入/导出名:

  • wasm-js: - 在所有模块导入名、模块名和导出名中保留。
  • wasm: - 在模块导入名和导出名中保留(允许导入的模块名,以支持未来的内置 polyfill)。

使用上述保留名称导入模块将抛出 WebAssembly.LinkError

顶层 await#

await 关键字可以在 ECMAScript 模块的顶层主体中使用。

假设有一个 a.mjs 文件,内容如下:

export const five = await Promise.resolve(5); 

还有一个 b.mjs 文件,内容如下:

import { five } from './a.mjs';

console.log(five); // Logs `5` 
node b.mjs # works 

如果一个顶层 await 表达式永远不解析,node 进程将以 13 状态码退出。

import { spawn } from 'node:child_process';
import { execPath } from 'node:process';

spawn(execPath, [
  '--input-type=module',
  '--eval',
  // Never-resolving Promise:
  'await new Promise(() => {})',
]).once('exit', (code) => {
  console.log(code); // Logs `13`
}); 

加载器#

之前的加载器文档现在位于模块:自定义钩子

解析和加载算法#

特性#

默认解析器具有以下属性:

  • 基于 FileURL 的解析,与 ES 模块使用的方式相同
  • 相对和绝对 URL 解析
  • 无默认扩展名
  • 无文件夹主文件
  • 通过 node_modules 查找裸说明符包
  • 对未知扩展名或协议不会失败
  • 可以可选地向加载阶段提供格式提示

默认加载器具有以下属性:

  • 通过 node: URL 支持内置模块加载
  • 通过 data: URL 支持“内联”模块加载
  • 支持 file: 模块加载
  • 对任何其他 URL 协议会失败
  • 对于 file: 加载,对未知扩展名会失败(仅支持 .cjs.js.mjs

解析算法#

加载 ES 模块说明符的算法通过下面的 ESM_RESOLVE 方法给出。它返回相对于 parentURL 的模块说明符的解析后 URL。

解析算法确定模块加载的完整解析 URL,以及其建议的模块格式。解析算法不确定解析的 URL 协议是否可以加载,或者文件扩展名是否被允许,这些验证由 Node.js 在加载阶段应用(例如,如果被要求加载一个协议不是 file:data:node: 的 URL)。

该算法还尝试根据扩展名确定文件格式(见下面的 ESM_FILE_FORMAT 算法)。如果它不识别文件扩展名(例如,如果不是 .mjs.cjs.json),则返回 undefined 的格式,这将在加载阶段抛出错误。

确定已解析 URL 的模块格式的算法由 ESM_FILE_FORMAT 提供,它为任何文件返回唯一的模块格式。对于 ECMAScript 模块,返回 "module" 格式,而 "commonjs" 格式用于指示通过遗留的 CommonJS 加载器加载。其他格式如 "addon" 可以在未来的更新中扩展。

在以下算法中,除非另有说明,所有子程序的错误都作为这些顶层例程的错误传播。

defaultConditions 是条件环境名称数组,["node", "import"]

解析器可能抛出以下错误:

  • 无效的模块说明符:模块说明符是无效的 URL、包名或包子路径说明符。
  • 无效的包配置:package.json 配置无效或包含无效配置。
  • 无效的包目标:包的 exports 或 imports 为包定义了一个无效类型或字符串的目标模块。
  • 包路径未导出:包的 exports 没有为给定模块定义或允许包中的目标子路径。
  • 包导入未定义:包的 imports 没有定义该说明符。
  • 未找到模块:请求的包或模块不存在。
  • 不支持的目录导入:解析的路径对应一个目录,这不是模块导入支持的目标。

解析算法规范#

ESM_RESOLVE(specifier, parentURL)

  1. resolvedundefined
  2. 如果 specifier 是一个有效的 URL,则
    1. resolved 设置为将 specifier 作为 URL 解析和重新序列化的结果。
  3. 否则,如果 specifier"/""./""../" 开头,则
    1. resolved 设置为相对于 parentURLspecifier 的 URL 解析结果。
  4. 否则,如果 specifier"#" 开头,则
    1. resolved 设置为 PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions) 的结果。
  5. 否则,
    1. 注意:specifier 现在是一个裸说明符。
    2. resolved 设置为 PACKAGE_RESOLVE(specifier, parentURL) 的结果。
  6. formatundefined
  7. 如果 resolved 是一个 "file:" URL,则
    1. 如果 resolved 包含任何 "/""\" 的百分号编码(分别为 "%2F""%5C"),则
      1. 抛出“无效的模块说明符”错误。
    2. 如果位于 resolved 的文件是一个目录,则
      1. 抛出“不支持的目录导入”错误。
    3. 如果位于 resolved 的文件不存在,则
      1. 抛出“未找到模块”错误。
    4. resolved 设置为 resolved 的真实路径,并保持相同的 URL 查询字符串和片段组件。
    5. format 设置为 ESM_FILE_FORMAT(resolved) 的结果。
  8. 否则,
    1. format 设置为与 URL resolved 关联的内容类型的模块格式。
  9. formatresolved 返回给加载阶段。

PACKAGE_RESOLVE(packageSpecifier, parentURL)

  1. packageNameundefined
  2. 如果 packageSpecifier 是一个空字符串,则
    1. 抛出“无效的模块说明符”错误。
  3. 如果 packageSpecifier 是一个 Node.js 内置模块名,则
    1. 返回字符串 "node:"packageSpecifier 的拼接结果。
  4. 如果 packageSpecifier 不以 "@" 开头,则
    1. packageName 设置为 packageSpecifier 直到第一个 "/" 分隔符或字符串末尾的子串。
  5. 否则,
    1. 如果 packageSpecifier 不包含 "/" 分隔符,则
      1. 抛出“无效的模块说明符”错误。
    2. packageName 设置为 packageSpecifier 直到第二个 "/" 分隔符或字符串末尾的子串。
  6. 如果 packageName"." 开头或包含 "\""%",则
    1. 抛出“无效的模块说明符”错误。
  7. packageSubpath"." 与从 packageName 长度位置开始的 packageSpecifier 子串的拼接结果。
  8. selfUrlPACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL) 的结果。
  9. 如果 selfUrl 不为 undefined,则返回 selfUrl
  10. parentURL 不是文件系统根目录时,
    1. packageURL"node_modules/"packageName 拼接后相对于 parentURL 的 URL 解析结果。
    2. parentURL 设置为 parentURL 的父文件夹 URL。
    3. 如果 packageURL 处的文件夹不存在,则
      1. 继续下一次循环迭代。
    4. pjsonREAD_PACKAGE_JSON(packageURL) 的结果。
    5. 如果 pjson 不为 nullpjson.exports 不为 nullundefined,则
      1. 返回 PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions) 的结果。
    6. 否则,如果 packageSubpath 等于 ".",则
      1. 如果 pjson.main 是一个字符串,则
        1. 返回 mainpackageURL 中的 URL 解析结果。
    7. 否则,
      1. 返回 packageSubpathpackageURL 中的 URL 解析结果。
  11. 抛出“未找到模块”错误。

PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)

  1. packageURLLOOKUP_PACKAGE_SCOPE(parentURL) 的结果。
  2. 如果 packageURLnull,则
    1. 返回 undefined
  3. pjsonREAD_PACKAGE_JSON(packageURL) 的结果。
  4. 如果 pjsonnull 或者 pjson.exportsnullundefined,则
    1. 返回 undefined
  5. 如果 pjson.name 等于 packageName,则
    1. 返回 PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions) 的结果。
  6. 否则,返回 undefined

PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)

注意:此函数由 CommonJS 解析算法直接调用。

  1. 如果 exports 是一个同时包含以 "." 开头的键和不以 "." 开头的键的对象,则抛出“无效的包配置”错误。
  2. 如果 subpath 等于 ".",则
    1. mainExportundefined
    2. 如果 exports 是一个字符串、数组,或不包含以 "." 开头的键的对象,则
      1. mainExport 设置为 exports
    3. 否则,如果 exports 是一个包含 "." 属性的对象,则
      1. mainExport 设置为 exports["."]。
    4. 如果 mainExport 不为 undefined,则
      1. resolvedPACKAGE_TARGET_RESOLVE( packageURL, mainExport, null, false, conditions) 的结果。
      2. 如果 resolved 不为 nullundefined,则返回 resolved
  3. 否则,如果 exports 是一个对象且 exports 的所有键都以 "." 开头,则
    1. 断言:subpath"./" 开头。
    2. resolvedPACKAGE_IMPORTS_EXPORTS_RESOLVE( subpath, exports, packageURL, false, conditions) 的结果。
    3. 如果 resolved 不为 nullundefined,则返回 resolved
  4. 抛出“包路径未导出”错误。

PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)

注意:此函数由 CommonJS 解析算法直接调用。

  1. 断言:specifier"#"开头。
  2. 如果 specifier 完全等于 "#" 或以 "#/" 开头,则
    1. 抛出“无效的模块说明符”错误。
  3. packageURLLOOKUP_PACKAGE_SCOPE(parentURL) 的结果。
  4. 如果 packageURL 不为 null,则
    1. pjsonREAD_PACKAGE_JSON(packageURL) 的结果。
    2. 如果 pjson.imports 是一个非 null 对象,则
      1. resolvedPACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions) 的结果。
      2. 如果 resolved 不为 nullundefined,则返回 resolved
  5. 抛出“包导入未定义”错误。

PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions)

  1. 如果 matchKey"/" 结尾,则
    1. 抛出“无效的模块说明符”错误。
  2. 如果 matchKeymatchObj 的一个键且不包含 "*",则
    1. targetmatchObj[matchKey] 的值。
    2. 返回 PACKAGE_TARGET_RESOLVE(packageURL, target, null, isImports, conditions) 的结果。
  3. expansionKeysmatchObj 中仅包含一个 "*" 的键列表,按 PATTERN_KEY_COMPARE 排序函数排序,该函数按特异性降序排列。
  4. 对于 expansionKeys 中的每个键 expansionKey,执行
    1. patternBaseexpansionKey 中第一个 "*" 字符之前(不包括)的子串。
    2. 如果 matchKeypatternBase 开头但不等于 patternBase,则
      1. patternTrailerexpansionKey 中第一个 "*" 字符之后索引处的子串。
      2. 如果 patternTrailer 长度为零,或者如果 matchKeypatternTrailer 结尾且 matchKey 的长度大于或等于 expansionKey 的长度,则
        1. targetmatchObj[expansionKey] 的值。
        2. patternMatchmatchKeypatternBase 长度的索引开始,到 matchKey 长度减去 patternTrailer 长度为止的子串。
        3. 返回 PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions) 的结果。
  5. 返回 null

PATTERN_KEY_COMPARE(keyA, keyB)

  1. 断言:keyA 只包含一个 "*"
  2. 断言:keyB 只包含一个 "*"
  3. baseLengthAkeyA"*" 的索引。
  4. baseLengthBkeyB"*" 的索引。
  5. 如果 baseLengthA 大于 baseLengthB,返回 -1。
  6. 如果 baseLengthB 大于 baseLengthA,返回 1。
  7. 如果 keyA 的长度大于 keyB 的长度,返回 -1。
  8. 如果 keyB 的长度大于 keyA 的长度,返回 1。
  9. 返回 0。

PACKAGE_TARGET_RESOLVE(packageURL, target, patternMatch, isImports, conditions)

  1. 如果 target 是一个字符串,则
    1. 如果 target 不以 "./" 开头,则
      1. 如果 isImportsfalse,或者如果 target"../""/" 开头,或者如果 target 是一个有效的 URL,则
        1. 抛出“无效的包目标”错误。
      2. 如果 patternMatch 是一个字符串,则
        1. 返回 PACKAGE_RESOLVE(将 target 中每个 "*" 替换为 patternMatch 的结果, packageURL + "/")。
      3. 返回 PACKAGE_RESOLVE(target, packageURL + "/")。
    2. 如果 target"/""\" 分割后,在第一个 "." 段之后包含任何 """.""..""node_modules" 段(不区分大小写,包括百分号编码的变体),则抛出“无效的包目标”错误。
    3. resolvedTargetpackageURLtarget 拼接后的 URL 解析结果。
    4. 断言:resolvedTarget 包含 packageURL
    5. 如果 patternMatchnull,则
      1. 返回 resolvedTarget
    6. 如果 patternMatch"/""\" 分割后包含任何 """.""..""node_modules" 段(不区分大小写,包括百分号编码的变体),则抛出“无效的模块说明符”错误。
    7. 返回将 resolvedTarget 中每个 "*" 替换为 patternMatch 后的 URL 解析结果。
  2. 否则,如果 target 是一个非 null 对象,则
    1. 如果 target 包含任何索引属性键(如 ECMA-262 6.1.7 数组索引 中定义),则抛出“无效的包配置”错误。
    2. 对于 target 的每个属性 p,按对象插入顺序,
      1. 如果 p 等于 "default"conditions 包含 p 的条目,则
        1. targetValuetargetp 属性的值。
        2. resolvedPACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions) 的结果。
        3. 如果 resolved 等于 undefined,则继续循环。
        4. 返回 resolved
    3. 返回 undefined
  3. 否则,如果 target 是一个数组,则
    1. 如果 _target.length 为零,返回 null
    2. 对于 target 中的每个项目 targetValue,执行
      1. resolvedPACKAGE_TARGET_RESOLVE( packageURL, targetValue, patternMatch, isImports, conditions) 的结果,如果遇到任何“无效的包目标”错误则继续循环。
      2. 如果 resolvedundefined,则继续循环。
      3. 返回 resolved
    3. 返回或抛出最后一个后备解析的 null 返回值或错误。
  4. 否则,如果 targetnull,则返回 null
  5. 否则抛出“无效的包目标”错误。

ESM_FILE_FORMAT(url)

  1. 断言:url 对应一个存在的文件。
  2. 如果 url".mjs" 结尾,则
    1. 返回 "module"
  3. 如果 url".cjs" 结尾,则
    1. 返回 "commonjs"
  4. 如果 url".json" 结尾,则
    1. 返回 "json"
  5. 如果 url".wasm" 结尾,则
    1. 返回 "wasm"
  6. 如果启用了 --experimental-addon-modules 并且 url".node" 结尾,则
    1. 返回 "addon"
  7. packageURLLOOKUP_PACKAGE_SCOPE(url) 的结果。
  8. pjsonREAD_PACKAGE_JSON(packageURL) 的结果。
  9. packageTypenull
  10. 如果 pjson?.type"module""commonjs",则
    1. packageType 设置为 pjson.type
  11. 如果 url".js" 结尾,则
    1. 如果 packageType 不为 null,则
      1. 返回 packageType
    2. 如果 DETECT_MODULE_SYNTAX(source) 的结果为 true,则
      1. 返回 "module"
    3. 返回 "commonjs"
  12. 如果 url 没有任何扩展名,则
    1. 如果 packageType"module" 并且位于 url 的文件包含 WebAssembly 模块的 "application/wasm" 内容类型头,则
      1. 返回 "wasm"
    2. 如果 packageType 不为 null,则
      1. 返回 packageType
    3. 如果 DETECT_MODULE_SYNTAX(source) 的结果为 true,则
      1. 返回 "module"
    4. 返回 "commonjs"
  13. 返回 undefined(将在加载阶段抛出错误)。

LOOKUP_PACKAGE_SCOPE(url)

  1. scopeURLurl
  2. scopeURL 不是文件系统根目录时,
    1. scopeURL 设置为 scopeURL 的父 URL。
    2. 如果 scopeURL"node_modules" 路径段结尾,则返回 null
    3. pjsonURL"package.json"scopeURL 内的解析结果。
    4. 如果 pjsonURL 处的文件存在,则
      1. 返回 scopeURL
  3. 返回 null

READ_PACKAGE_JSON(packageURL)

  1. pjsonURL"package.json"packageURL 中的解析结果。
  2. 如果 pjsonURL 处的文件不存在,则
    1. 返回 null
  3. 如果 packageURL 处的文件无法解析为有效的 JSON,则
    1. 抛出“无效的包配置”错误。
  4. 返回位于 pjsonURL 的文件的已解析 JSON 源。

DETECT_MODULE_SYNTAX(source)

  1. source 解析为 ECMAScript 模块。
  2. 如果解析成功,则
    1. 如果 source 包含顶层 await、静态 importexport 语句,或 import.meta,则返回 true
    2. 如果 source 包含任何 CommonJS 包装器变量(requireexportsmodule__filename__dirname)的顶层词法声明(constletclass),则返回 true
  3. 否则返回 false

自定义 ESM 说明符解析算法#

模块自定义钩子提供了一种自定义 ESM 说明符解析算法的机制。一个为 ESM 说明符提供 CommonJS 风格解析的示例是 commonjs-extension-resolution-loader