Node.js v14.18.1 文档


目录

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';

// 打印: 6
console.log(addTwo(4));

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

启用#

中英对照

Node.js 默认将 JavaScript 代码视为 CommonJS 模块。 作者可以通过 .mjs 文件扩展名、package.json "type" 字段、或 --input-type 标志告诉 Node.js 将 JavaScript 代码视为 ECMAScript 模块。 有关更多详细信息,请参阅包模块

#

中英对照

此章节已移至包模块

import 说明符#

术语#

中英对照

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

有三种类型的说明符:

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

  • 裸说明符,如 'some-package''some-package/shuffle'。 它们可以通过包名称来引用包的主要入口点,或者根据示例分别以包名称为前缀的包中的特定功能模块。 包括文件扩展名仅适用于没有 "exports" 字段的包。

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

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

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

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

强制的文件扩展名#

中英对照

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

此行为与 import 在浏览器环境中的行为方式相匹配,假设服务器是典型配置的。

URL#

中英对照

ES 模块被解析并缓存为 URL。 这意味着包含 #? 等特殊字符的文件需要转义。

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

file: URL#

中英对照

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

import './foo.mjs?query=1'; // 加载具有 "?query=1" 查询的 ./foo.mjs
import './foo.mjs?query=2'; // 加载具有 "?query=2" 查询的 ./foo.mjs

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

data: 导入#

中英对照

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

  • text/javascript 用于 ES 模块
  • application/json 用于 JSON
  • application/wasm 用于 Wasm

data: URL 只为内置模块解析裸说明符绝对说明符。 解析相对说明符不起作用,因为 data: 不是特殊协议。 例如,尝试从 data:text/javascript,import "./foo"; 加载 ./foo 无法解析,因为 data: URL 没有相对解析的概念。 正在使用的 data: URL 示例是:

import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"';
node: 导入#

中英对照

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

import fs from 'node:fs/promises';

内置模块#

中英对照

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

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

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

fs.readFileSync === readFileSync;

import() 表达式#

中英对照

动态的 import() 在 CommonJS 和 ES 模块中都受支持。 在 CommonJS 模块中它可以用来加载 ES 模块。

import.meta#

中英对照

import.meta 元属性是包含以下属性的 Object

import.meta.url#

中英对照

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

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

这可以启用有用的模式,例如相对文件加载

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

import.meta.resolve(specifier[, parent])#

中英对照

稳定性: 1 - 实验

此特性仅在启用 --experimental-import-meta-resolve 命令标志时可用。

  • specifier <string> 相对于 parent 解析的模块说明符。
  • parent <string> 要解析的绝对的父模块 URL。 如果未指定,则使用 import.meta.url 的值作为默认值。
  • 返回: <Promise>

提供作用域为每个模块的模块相关解析函数,返回 URL 字符串。

const dependencyAsset = await import.meta.resolve('component-lib/asset.css');

import.meta.resolve 还接受第二个参数,它是从中解析的父模块:

await import.meta.resolve('./dep', import.meta.url);

此函数是异步的,因为 Node.js 中的 ES 模块解析器是允许异步的。

与 CommonJS 的互操作性#

import 声明#

中英对照

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

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

require#

中英对照

CommonJS 模块 require 总是将它引用的文件视为 CommonJS。

不支持使用 require 加载 ES 模块,因为 ES 模块具有异步执行。 而是,使用 import() 从 CommonJS 模块加载 ES 模块。

CommonJS 命名空间#

中英对照

CommonJS 模块由可以是任何类型的 module.exports 对象组成。

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

import { default as cjs } from 'cjs';

// 下面的导入语句是上面的导入语句中
// `{ default as cjsSugar }` 的 "语法糖"(等价但更甜):
import cjsSugar from 'cjs';

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

CommonJS 模块的 ECMAScript 模块命名空间表示始终是使用 default 导出键指向 CommonJS module.exports 值的命名空间。

当使用 import * as m from 'cjs' 或动态导入时,可以直接观察到此模块命名空间外来对象:

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

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

例如,考虑编写的 CommonJS 模块:

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

前面的模块支持 ES 模块中的命名导入:

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

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

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

从上一个记录模块命名空间外来对象的示例中可以看出,name 导出是从 module.exports 对象复制出来的,并在导入模块时直接设置在 ES 模块命名空间上。

未检测到这些命名导出的实时绑定更新或添加到 module.exports 的新导出。

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

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

ES 模块和 CommonJS 之间的差异#

没有 require、exports 或 module.exports#

中英对照

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

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

没有 __filename 或 __dirname#

中英对照

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

__filename__dirname 用例可以通过 import.meta.url 复制。

没有 JSON 模块加载#

中英对照

JSON 导入仍处于实验阶段,仅通过 --experimental-json-modules 标志支持。

本地 JSON 文件可以直接用 fs 相对于 import.meta.url 加载:

import { readFile } from 'fs/promises';
const json = JSON.parse(await readFile(new URL('./dat.json', import.meta.url)));

也可以使用 module.createRequire()

没有原生模块加载#

中英对照

ES 模块导入当前不支持原生模块。

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

没有 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 模块#

中英对照

稳定性: 1 - 实验

目前导入 JSON 模块仅支持 commonjs 模式,并且使用 CJS 加载器加载。 WHATWG JSON 模块规范仍在标准化中,并且通过在运行 Node.js 时包含额外的标志 --experimental-json-modules 进行实验性地支持。

当包含 --experimental-json-modules 标志时,commonjsmodule 模式都使用新的实验性 JSON 加载器。 导入的 JSON 只暴露 default。 不支持命名导出。 在 CommonJS 缓存中创建缓存条目,以避免重复。 如果 JSON 模块已经从同一路径导入,则在 CommonJS 中返回相同的对象。

假设 index.mjs 具有

import packageConfig from './package.json';

模块需要 --experimental-json-modules 标志才有效。

node index.mjs # 失败
node --experimental-json-modules index.mjs # 有效

Wasm 模块#

中英对照

稳定性: 1 - 实验

--experimental-wasm-modules 标志下支持导入 Web Assembly 模块,允许将任何 .wasm 文件作为普通模块导入,同时还支持它们的模块导入。

此集成符合用于 Web Assembly 的 ES 模块集成提案

例如,index.mjs 包含:

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

在以下条件下执行:

node --experimental-wasm-modules index.mjs

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

顶层的 await#

中英对照

稳定性: 1 - 实验

根据 ECMAScript 顶层 await 提案await 关键字可用于模块内的顶层(异步函数之外)。

假设 a.mjs 具有

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

并且 b.mjs 具有

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

console.log(five); // 记录 `5`
node b.mjs # 有效

加载器#

中英对照

稳定性: 1 - 实验

注意:此 API 目前正在重新设计,并将继续更改。

要自定义默认的模块解析,则可以选择通过 Node.js 的 --experimental-loader ./loader-name.mjs 参数提供加载器钩子。

当使用钩子时,只适用于 ES 模块加载,而不适用于任何加载的 CommonJS 模块。

钩子#

resolve(specifier, context, defaultResolve)#

中英对照

注意:加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。

resolve 钩子返回给定模块说明符和父 URL 的解析文件 URL。 模块说明符是 import 语句或 import() 表达式中的字符串,父 URL 是导入此模块的 URL,如果这是应用程序的主要入口点,则为 undefined

context 上的 conditions 属性是适用于此解析请求的条件导出的条件数组。 它们可用于在别处查找条件映射或在调用默认解析逻辑时修改列表。

当前的包导出条件始终在传入钩子的 context.conditions 数组中。 为了在调用 defaultResolve 时保证默认的 Node.js 模块说明符解析行为,传给它的 context.conditions 数组必须包含最初传到 resolve 钩子的 context.conditions 数组的所有元素。

/**
 * @param {string} specifier
 * @param {{
 *   conditions: !Array<string>,
 *   parentURL: !(string | undefined),
 * }} context
 * @param {Function} defaultResolve
 * @returns {Promise<{ url: string }>}
 */
export async function resolve(specifier, context, defaultResolve) {
  const { parentURL = null } = context;
  if (Math.random() > 0.5) { // 一些条件。
    // 对于部分或全部说明符,做一些自定义逻辑来解决。
    // 总是返回 {url: <string>} 形式的对象。
    return {
      url: parentURL ?
        new URL(specifier, parentURL).href :
        new URL(specifier).href,
    };
  }
  if (Math.random() < 0.5) { // 另一个条件。
    // 当调用 `defaultResolve` 时,可以修改参数。
    // 在这种情况下,它为匹配条件导出添加了另一个值。
    return defaultResolve(specifier, {
      ...context,
      conditions: [...context.conditions, 'another-condition'],
    });
  }
  // 对于所有其他说明符,请遵循 Node.js。
  return defaultResolve(specifier, context, defaultResolve);
}
getFormat(url, context, defaultGetFormat)#

中英对照

注意:加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。

getFormat 钩子提供了一种方式来定义确定网址应如何解释的自定义方法。 返回的 format 也会影响解析时模块的可接受的源值形式。 这可以是以下之一:

format描述getSourcetransformSource 返回的 source 的可接受类型
'builtin'加载 Node.js 内置模块不适用
'commonjs'加载 Node.js CommonJS 模块不适用
'json'加载 JSON 文件{ string, ArrayBuffer, TypedArray }
'module'加载 ES 模块{ string, ArrayBuffer, TypedArray }
'wasm'加载 WebAssembly 模块{ ArrayBuffer, TypedArray }

注意:这些类型都对应于 ECMAScript 中定义的类。

注意:如果基于文本的格式(即 'json''module')的源值不是字符串,则使用 util.TextDecoder 将其转换为字符串。

/**
 * @param {string} url
 * @param {Object} context (currently empty)
 * @param {Function} defaultGetFormat
 * @returns {Promise<{ format: string }>}
 */
export async function getFormat(url, context, defaultGetFormat) {
  if (Math.random() > 0.5) { // 一些条件。
    // 对于部分或所有 URL,执行一些自定义逻辑来确定格式。
    // 始终返回 {format: <string>} 形式的对象,
    // 其中格式是上表中的字符串之一。
    return {
      format: 'module',
    };
  }
  // 所有其他 URL 都遵循 Node.js。
  return defaultGetFormat(url, context, defaultGetFormat);
}
getSource(url, context, defaultGetSource)#

中英对照

注意:加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。

getSource 钩子提供了一种方法来定义用于检索 ES 模块说明符的源代码的自定义方法。 这将允许加载器潜在地避免从磁盘读取文件。

/**
 * @param {string} url
 * @param {{ format: string }} context
 * @param {Function} defaultGetSource
 * @returns {Promise<{ source: !(string | SharedArrayBuffer | Uint8Array) }>}
 */
export async function getSource(url, context, defaultGetSource) {
  const { format } = context;
  if (Math.random() > 0.5) { // 一些条件。
    // 对于部分或所有 URL,执行一些自定义逻辑来检索源。
    // 总是返回 {source: <string|buffer>} 形式的对象。
    return {
      source: '...',
    };
  }
  // 所有其他 URL 都遵循 Node.js。
  return defaultGetSource(url, context, defaultGetSource);
}
transformSource(source, context, defaultTransformSource)#

中英对照

注意:加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。

transformSource 钩子提供了一种在加载源字符串之后但在 Node.js 对其进行任何操作之前修改加载的 ES 模块文件的源代码的方法。

如果此钩子用于将未知的 Node.js 文件类型转换为可执行的 JavaScript,则还需要解析钩子来注册任何未知的 Node.js 文件扩展名。 请参阅下面的转译加载器示例

/**
 * @param {!(string | SharedArrayBuffer | Uint8Array)} source
 * @param {{
 *   format: string,
 *   url: string,
 * }} context
 * @param {Function} defaultTransformSource
 * @returns {Promise<{ source: !(string | SharedArrayBuffer | Uint8Array) }>}
 */
export async function transformSource(source, context, defaultTransformSource) {
  const { url, format } = context;
  if (Math.random() > 0.5) { // 一些条件。
    // 对于部分或全部 URL,做一些修改源的自定义逻辑。
    // 总是返回 {source: <string|buffer>} 形式的对象。
    return {
      source: '...',
    };
  }
  // 对于所有其他来源,请遵循 Node.js。
  return defaultTransformSource(source, context, defaultTransformSource);
}
getGlobalPreloadCode()#

中英对照

注意:加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。

有时可能需要在应用程序运行所在的同一全局范围内运行一些代码。 此钩子允许返回在启动时作为宽松模式脚本运行的字符串。

类似于 CommonJS 封装器的工作方式,代码在隐式函数范围内运行。 唯一的参数是类似 require 的函数,可用于加载内置函数,如 "fs":getBuiltin(request: string)

如果代码需要更高级的 require 特性,则必须使用 module.createRequire() 构建自己的 require

/**
 * @returns {string} 在应用程序启动之前运行的代码
 */
export function getGlobalPreloadCode() {
  return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');

const { createRequire } = getBuiltin('module');

const require = createRequire(process.cwd() + '/<preload>');
// [...]
`;
}

示例#

中英对照

各种加载器钩子可以一起使用来完成对 Node.js 代码加载和评估行为的广泛定制。

HTTPS 加载器#

中英对照

在当前的 Node.js 中,不支持以 https:// 开头的说明符。 下面的加载器注册钩子以启用对此类说明符的基本支持。 虽然这似乎是对 Node.js 核心功能的重大改进,但实际使用这个加载器有很大的缺点:性能比从磁盘加载文件慢得多,没有缓存,也没有安全性。

// https-loader.mjs
import { get } from 'https';

export function resolve(specifier, context, defaultResolve) {
  const { parentURL = null } = context;

  // 通常,Node.js 会在以 'https://' 开头的说明符上出错,
  // 因此此钩子会拦截它们并将它们转换为绝对 URL,
  // 以便传给下面的后面的钩子。
  if (specifier.startsWith('https://')) {
    return {
      url: specifier
    };
  } else if (parentURL && parentURL.startsWith('https://')) {
    return {
      url: new URL(specifier, parentURL).href
    };
  }

  // 让 Node.js 处理所有其他说明符。
  return defaultResolve(specifier, context, defaultResolve);
}

export function getFormat(url, context, defaultGetFormat) {
  // 此加载器假定所有网络提供的 JavaScript 都是 ES 模块代码。
  if (url.startsWith('https://')) {
    return {
      format: 'module'
    };
  }

  // 让 Node.js 处理所有其他 URL。
  return defaultGetFormat(url, context, defaultGetFormat);
}

export function getSource(url, context, defaultGetSource) {
  // 要通过网络加载 JavaScript,
  // 则需要获取并返回它。
  if (url.startsWith('https://')) {
    return new Promise((resolve, reject) => {
      get(url, (res) => {
        let data = '';
        res.on('data', (chunk) => data += chunk);
        res.on('end', () => resolve({ source: data }));
      }).on('error', (err) => reject(err));
    });
  }

  // 让 Node.js 处理所有其他 URL。
  return defaultGetSource(url, context, defaultGetSource);
}
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';

console.log(VERSION);

使用前面的加载器,运行 node --experimental-loader ./https-loader.mjs ./main.mjs 会在 main.mjs 中的 URL 处按照模块打印当前版本的 CoffeeScript。

转译器加载器#

中英对照

可以使用 transformSource 钩子将 Node.js 无法理解的格式的源转换为 JavaScript。 但是,在调用该钩子之前,其他钩子需要告诉 Node.js 不要在未知文件类型上抛出错误;并告诉 Node.js 如何加载这种新文件类型。

这比在运行 Node.js 之前转译源文件的性能要低;转译加载器应该只用于开发和测试目的。

// coffeescript-loader.mjs
import { URL, pathToFileURL } from 'url';
import CoffeeScript from 'coffeescript';

const baseURL = pathToFileURL(`${process.cwd()}/`).href;

// CoffeeScript 文件以 .coffee、.litcoffee 或 .coffee.md 结尾。
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;

export function resolve(specifier, context, defaultResolve) {
  const { parentURL = baseURL } = context;

  // Node.js 通常在未知文件扩展名上出错,
  // 因此返回以 CoffeeScript 文件扩展名结尾的说明符的 URL。
  if (extensionsRegex.test(specifier)) {
    return {
      url: new URL(specifier, parentURL).href
    };
  }

  // 让 Node.js 处理所有其他说明符。
  return defaultResolve(specifier, context, defaultResolve);
}

export function getFormat(url, context, defaultGetFormat) {
  // 现在修补了解决以让 CoffeeScript URL 通过,
  // 需要告诉 Node.js 这样的 URL 应该被解释为什么格式。
  // 为了这个加载器的目的,所有 CoffeeScript URL 都是 ES 模块。
  if (extensionsRegex.test(url)) {
    return {
      format: 'module'
    };
  }

  // 让 Node.js 处理所有其他 URL。
  return defaultGetFormat(url, context, defaultGetFormat);
}

export function transformSource(source, context, defaultTransformSource) {
  const { url, format } = context;

  if (extensionsRegex.test(url)) {
    return {
      source: CoffeeScript.compile(source, { bare: true })
    };
  }

  // 让 Node.js 处理所有其他来源。
  return defaultTransformSource(source, context, defaultTransformSource);
}
# main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'

import { version } from 'process'
console.log "Brought to you by Node.js version #{version}"
# scream.coffee
export scream = (str) -> str.toUpperCase()

使用前面的加载器,运行 node --experimental-loader ./coffeescript-loader.mjs main.coffee 会导致 main.coffee 在其源代码从磁盘加载之后但在 Node.js 执行之前转换为 JavaScript;对于通过任何加载文件的 import 语句引用的任何 .coffee.litcoffee.coffee.md 文件,依此类推。

解析算法#

特性#

中英对照

解析器具有以下属性:

  • ES 模块使用的基于 FileURL 的解析
  • 支持内置模块加载
  • 相对和绝对的网址解析
  • 没有默认的扩展名
  • 没有主文件夹
  • 通过 node_modules 进行裸说明符包解析查找

解析器算法#

中英对照

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

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

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

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

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

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

解析器算法规范#

ESM_RESOLVE(specifier, parentURL)

  1. Let resolved be undefined.
  2. If specifier is a valid URL, then
    1. Set resolved to the result of parsing and reserializing specifier as a URL.
  3. Otherwise, if specifier starts with "/", "./" or "../", then
    1. Set resolved to the URL resolution of specifier relative to parentURL.
  4. Otherwise, if specifier starts with "#", then
    1. Set resolved to the destructured value of the result of PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
  5. Otherwise,
    1. Note: specifier is now a bare specifier.
    2. Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
  6. If resolved contains any percent encodings of "/" or "\" ("%2f" and "%5C" respectively), then
    1. Throw an Invalid Module Specifier error.
  7. If the file at resolved is a directory, then
    1. Throw an Unsupported Directory Import error.
  8. If the file at resolved does not exist, then
    1. Throw a Module Not Found error.
  9. Set resolved to the real path of resolved.
  10. Let format be the result of ESM_FORMAT(resolved).
  11. Load resolved as module format, format.
  12. Return resolved.

PACKAGE_RESOLVE(packageSpecifier, parentURL)

  1. Let packageName be undefined.
  2. If packageSpecifier is an empty string, then
    1. Throw an Invalid Module Specifier error.
  3. If packageSpecifier does not start with "@", then
    1. Set packageName to the substring of packageSpecifier until the first "/" separator or the end of the string.
  4. Otherwise,
    1. If packageSpecifier does not contain a "/" separator, then
      1. Throw an Invalid Module Specifier error.
    2. Set packageName to the substring of packageSpecifier until the second "/" separator or the end of the string.
  5. If packageName starts with "." or contains "\" or "%", then
    1. Throw an Invalid Module Specifier error.
  6. Let packageSubpath be "." concatenated with the substring of packageSpecifier from the position at the length of packageName.
  7. Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
  8. If selfUrl is not undefined, return selfUrl.
  9. If packageSubpath is "." and packageName is a Node.js builtin module, then
    1. Return the string "node:" concatenated with packageSpecifier.
  10. While parentURL is not the file system root,
    1. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
    2. Set parentURL to the parent folder URL of parentURL.
    3. If the folder at packageURL does not exist, then
      1. Set parentURL to the parent URL path of parentURL.
      2. Continue the next loop iteration.
    4. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
    5. If pjson is not null and pjson.exports is not null or undefined, then
      1. Let exports be pjson.exports.
      2. Return the resolved destructured value of the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
    6. Otherwise, if packageSubpath is equal to ".", then
      1. Return the result applying the legacy LOAD_AS_DIRECTORY CommonJS resolver to packageURL, throwing a Module Not Found error for no resolution.
    7. Otherwise,
      1. Return the URL resolution of packageSubpath in packageURL.
  11. Throw a Module Not Found error.

PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)

  1. Let packageURL be the result of READ_PACKAGE_SCOPE(parentURL).
  2. If packageURL is null, then
    1. Return undefined.
  3. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
  4. If pjson is null or if pjson.exports is null or undefined, then
    1. Return undefined.
  5. If pjson.name is equal to packageName, then
    1. Return the resolved destructured value of the result of PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, pjson.exports, defaultConditions).
  6. Otherwise, return undefined.

PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)

  1. If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error.
  2. If subpath is equal to ".", then
    1. Let mainExport be undefined.
    2. If exports is a String or Array, or an Object containing no keys starting with ".", then
      1. Set mainExport to exports.
    3. Otherwise if exports is an Object containing a "." property, then
      1. Set mainExport to exports["."].
    4. If mainExport is not undefined, then
      1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, "", false, false, conditions).
      2. If resolved is not null or undefined, then
        1. Return resolved.
  3. Otherwise, if exports is an Object and all keys of exports start with ".", then
    1. Let matchKey be the string "./" concatenated with subpath.
    2. Let resolvedMatch be result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
    3. If resolvedMatch.resolve is not null or undefined, then
      1. Return resolvedMatch.
  4. Throw a Package Path Not Exported error.

PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)

  1. Assert: specifier begins with "#".
  2. If specifier is exactly equal to "#" or starts with "#/", then
    1. Throw an Invalid Module Specifier error.
  3. Let packageURL be the result of READ_PACKAGE_SCOPE(parentURL).
  4. If packageURL is not null, then
    1. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
    2. If pjson.imports is a non-null Object, then
      1. Let resolvedMatch be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE(specifier, pjson.imports, packageURL, true, conditions).
      2. If resolvedMatch.resolve is not null or undefined, then
        1. Return resolvedMatch.
  5. Throw a Package Import Not Defined error.

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

  1. If matchKey is a key of matchObj, and does not end in "*", then
    1. Let target be the value of matchObj[matchKey].
    2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, target, "", false, isImports, conditions).
    3. Return the object { resolved, exact: true }.
  2. Let expansionKeys be the list of keys of matchObj ending in "/" or "*", sorted by length descending.
  3. For each key expansionKey in expansionKeys, do
    1. If expansionKey ends in "*" and matchKey starts with but is not equal to the substring of expansionKey excluding the last "*" character, then
      1. Let target be the value of matchObj[expansionKey].
      2. Let subpath be the substring of matchKey starting at the index of the length of expansionKey minus one.
      3. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, true, isImports, conditions).
      4. Return the object { resolved, exact: true }.
    2. If matchKey starts with expansionKey, then
      1. Let target be the value of matchObj[expansionKey].
      2. Let subpath be the substring of matchKey starting at the index of the length of expansionKey.
      3. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, false, isImports, conditions).
      4. Return the object { resolved, exact: false }.
  4. Return the object { resolved: null, exact: true }.

PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, pattern, internal, conditions)

  1. If target is a String, then
    1. If pattern is false, subpath has non-zero length and target does not end with "/", throw an Invalid Module Specifier error.
    2. If target does not start with "./", then
      1. If internal is true and target does not start with "../" or "/" and is not a valid URL, then
        1. If pattern is true, then
          1. Return PACKAGE_RESOLVE(target with every instance of "*" replaced by subpath, packageURL + "/")_.
        2. Return PACKAGE_RESOLVE(target + subpath, packageURL + "/")_.
      2. Otherwise, throw an Invalid Package Target error.
    3. If target split on "/" or "\" contains any ".", ".." or "node_modules" segments after the first segment, throw an Invalid Package Target error.
    4. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
    5. Assert: resolvedTarget is contained in packageURL.
    6. If subpath split on "/" or "\" contains any ".", ".." or "node_modules" segments, throw an Invalid Module Specifier error.
    7. If pattern is true, then
      1. Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
    8. Otherwise,
      1. Return the URL resolution of the concatenation of subpath and resolvedTarget.
  2. Otherwise, if target is a non-null Object, then
    1. If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
    2. For each property p of target, in object insertion order as,
      1. If p equals "default" or conditions contains an entry for p, then
        1. Let targetValue be the value of the p property in target.
        2. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, subpath, pattern, internal, conditions).
        3. If resolved is equal to undefined, continue the loop.
        4. Return resolved.
    3. Return undefined.
  3. Otherwise, if target is an Array, then
    1. If _target.length is zero, return null.
    2. For each item targetValue in target, do
      1. Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, subpath, pattern, internal, conditions), continuing the loop on any Invalid Package Target error.
      2. If resolved is undefined, continue the loop.
      3. Return resolved.
    3. Return or throw the last fallback resolution null return or error.
  4. Otherwise, if target is null, return null.
  5. Otherwise throw an Invalid Package Target error.

ESM_FORMAT(url)

  1. Assert: url corresponds to an existing file.
  2. Let pjson be the result of READ_PACKAGE_SCOPE(url).
  3. If url ends in ".mjs", then
    1. Return "module".
  4. If url ends in ".cjs", then
    1. Return "commonjs".
  5. If pjson?.type exists and is "module", then
    1. If url ends in ".js", then
      1. Return "module".
    2. Throw an Unsupported File Extension error.
  6. Otherwise,
    1. Throw an Unsupported File Extension error.

READ_PACKAGE_SCOPE(url)

  1. Let scopeURL be url.
  2. While scopeURL is not the file system root,
    1. Set scopeURL to the parent URL of scopeURL.
    2. If scopeURL ends in a "node_modules" path segment, return null.
    3. Let pjson be the result of READ_PACKAGE_JSON(scopeURL).
    4. If pjson is not null, then
      1. Return pjson.
  3. Return null.

READ_PACKAGE_JSON(packageURL)

  1. Let pjsonURL be the resolution of "package.json" within packageURL.
  2. If the file at pjsonURL does not exist, then
    1. Return null.
  3. If the file at packageURL does not parse as valid JSON, then
    1. Throw an Invalid Package Configuration error.
  4. Return the parsed JSON source of the file at pjsonURL.

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

中英对照

稳定性: 1 - 实验

当前的说明符解析不支持 CommonJS 加载器的所有默认行为。 行为差异之一是文件扩展名的自动解析以及导入具有索引文件的目录的能力。

--experimental-specifier-resolution=[mode] 标志可用于自定义扩展解析算法。 默认模式是 explicit,这需要向加载器提供模块的完整路径。 要启用自动扩展解析并从包含索引文件的目录导入,则使用 node 模式。

$ node index.mjs
success!
$ node index # 失败!
Error: Cannot find module
$ node --experimental-specifier-resolution=node index
success!
返回顶部