Node.js v18.6.0 文档


目录

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 有两个模块系统:CommonJS 模块和 ECMAScript 模块。

作者可以通过 .mjs 文件扩展名、package.json "type" 字段、或 --input-type 标志告诉 Node.js 使用 ECMAScript 模块加载器。 在这些情况之外,Node.js 将使用 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 模块解析算法处理。 所有其他说明符解析始终仅使用标准的相对网址解析语义进行解析。

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

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

强制的文件扩展名#

中英对照

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

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

URL#

中英对照

ES 模块被解析并缓存为 URL。 这意味着特殊字符必须是百分比编码的,比如使用 %23#、使用 %3F?

支持 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
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' assert { 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';

导入断言#

中英对照

稳定性: 1 - 实验

导入断言提案为模块导入语句添加了内联语法,以便在模块说明符旁边传入更多信息。

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

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

Node.js 支持以下 type 值,其断言是强制性的:

断言 type用于
'json'JSON 模块

内置模块#

中英对照

核心模块提供了其公共 API 的命名导出。 还提供了默认导出,其是 CommonJS 导出的值。 默认导出可用于修改命名导出等。 内置模块的命名导出仅通过调用 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

import.meta.url#

中英对照

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

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

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

import { readFileSync } from 'node: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> 要解析的绝对的父模块 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 复制。

没有原生模块加载#

中英对照

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 - 实验

import 可以引用 JSON 文件:

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

assert { type: 'json' } 语法是强制性的;参见导入断言

导入的 JSON 只暴露一个 default 导出。 不支持命名导出。 在 CommonJS 缓存中创建缓存条目,以避免重复。 如果 JSON 模块已经从同一路径导入,则在 CommonJS 中返回相同的对象。

Wasm 模块#

中英对照

稳定性: 1 - 实验

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

此集成符合 WebAssembly 的 ES 模块集成提案

例如,index.mjs 包含:

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

在以下条件下执行:

node --experimental-wasm-modules index.mjs

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

顶层的 await#

中英对照

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

假设 a.mjs 具有

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

并且 b.mjs 具有

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

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

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

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

spawn(execPath, [
  '--input-type=module',
  '--eval',
  // 永不解决的 Promise:
  'await new Promise(() => {})',
]).once('exit', (code) => {
  console.log(code); // 记录 `13`
});

HTTPS 和 HTTP 导入#

中英对照

稳定性: 1 - 实验

--experimental-network-imports 标志下支持使用 https:http: 导入基于网络的模块。 这允许类似网络浏览器的导入在 Node.js 中工作,但由于应用程序稳定性和安全问题在特权环境而不是浏览器沙箱中运行时会有所不同,因此存在一些差异。

导入仅限于 HTTP/1#

中英对照

尚不支持 HTTP/2 和 HTTP/3 的自动协议协商。

HTTP 仅限于环回地址#

中英对照

http: 易受中间人攻击,不允许用于 IPv4 地址 127.0.0.0/8127.0.0.1127.255.255.255)和 IPv6 地址 ::1 之外的地址。 对 http: 的支持旨在用于本地开发。

永远不会发送身份验证到目标服务器#

中英对照

AuthorizationCookieProxy-Authorization 标头未发送到服务器。 避免在部分导入的 URL 中包含用户信息。 正在研究在服务器上安全使用这些的安全模型。

永远不会在目标服务器上检查 CORS#

中英对照

CORS 旨在允许服务器将 API 的使用者限制为一组特定的主机。 这不受支持,因为它对于基于服务器的实现没有意义。

无法加载非网络依赖项#

中英对照

这些模块不能访问不超过 http:https: 的其他模块。 要在避免安全问题的同时仍然访问本地模块,则传入对本地依赖项的引用:

// file.mjs
import worker_threads from 'node:worker_threads';
import { configure, resize } from 'https://example.com/imagelib.mjs';
configure({ worker_threads });
// https://example.com/imagelib.mjs
let worker_threads;
export function configure(opts) {
  worker_threads = opts.worker_threads;
}
export function resize(img, size) {
  // 在工作线程中调整大小以避免主线程阻塞
}

默认情况下不启用基于网络的加载#

中英对照

目前,需要 --experimental-network-imports 标志来启用通过 http:https: 加载资源。 将来,将使用不同的机制来执行此操作。 需要选择加入以防止不经意间使用可能影响 Node.js 应用程序可靠性的潜在可变状态的传递依赖关系。

加载器#

中英对照

稳定性: 1 - 实验

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

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

当使用钩子时,它们适用于入口点和所有 import 调用。 它们不适用于 require 调用;那些仍然遵循 CommonJS 规则。

加载器遵循 --require 的模式:

node \
  --experimental-loader unpkg \
  --experimental-loader http-to-https \
  --experimental-loader cache-buster

这些按以下顺序调用:cache-buster 调用 http-to-httpshttp-to-https 调用 unpkg

钩子#

中英对照

钩子是链的一部分,即使该链仅由一个自定义(用户提供的)钩子和始终存在的默认钩子组成。 钩子函数嵌套:每个函数都必须返回一个普通对象,并且链接发生在每个函数调用 next<hookName>() 的结果中,next<hookName>() 是对后续加载程序钩子的引用。

返回缺少必需属性的值的钩子会触发异常。 不调用 next<hookName>() 且不返回 shortCircuit: true 的钩子也会触发异常。 这些错误有助于防止链中的意外中断。

resolve(specifier, context, nextResolve)#

中英对照

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

  • specifier <string>
  • context <Object>
    • conditions <string[]> 相关 package.json 的导出条件
    • importAssertions <Object>
    • parentURL <string> | <undefined> 导入此模块的模块,如果这是 Node.js 入口点,则为未定义
  • nextResolve <Function> 链中后续的 resolve 钩子,或者用户提供的最后一个 resolve 钩子之后的 Node.js 默认 resolve 钩子
  • 返回: <Object>
    • format <string> | <null> | <undefined> 加载钩子的提示(可能会被忽略)'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
    • shortCircuit <undefined> | <boolean> 此钩子打算终止 resolve 钩子链的信号。 默认值: false
    • url <string> 此输入解析到的绝对 URL

resolve 钩子链负责解析给定模块说明符和父 URL 的文件 URL,以及可选的格式(例如 'module')作为对 load 钩子的提示。 如果指定了格式,则 load 钩子最终负责提供最终的 format 值(可以随意忽略 resolve 提供的提示);如果 resolve 提供了 format,则需要自定义 load 钩子,即使只是通过 Node.js 默认 load 钩子的值。

模块说明符是 import 语句或 import() 表达式中的字符串。

父 URL 是导入此模块的模块的 URL,如果这是应用程序的主入口点,则为 undefined

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

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

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

  if (Math.random() > 0.5) { // 一些条件。
    // 对于部分或全部说明符,做一些自定义逻辑来解决。
    // 总是返回 {url: <string>} 形式的对象。
    return {
      shortCircuit: true,
      url: parentURL ?
        new URL(specifier, parentURL).href :
        new URL(specifier).href,
    };
  }

  if (Math.random() < 0.5) { // 另一个条件。
    // 当调用 `defaultResolve` 时,可以修改参数。
    // 在这种情况下,它为匹配条件导出添加了另一个值。
    return nextResolve(specifier, {
      ...context,
      conditions: [...context.conditions, 'another-condition'],
    });
  }

  // 推迟到链中的下一个钩子,这将是
  // 如果这是最后一个用户指定的加载器,Node.js 默认解析。
  return nextResolve(specifier);
}
load(url, context, nextLoad)#

中英对照

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

在此 API 的先前版本中,它被拆分为 3 个单独的、现已弃用的钩子(getFormatgetSourcetransformSource)。

load 钩子提供了一种方式来定义确定网址应如何解释、检索、以及解析的自定义方法。 它还负责验证导入断言。

format 的最终值必须是以下之一:

format描述Acceptable types for source returned by load
'builtin'加载 Node.js 内置模块不适用
'commonjs'加载 Node.js CommonJS 模块不适用
'json'加载 JSON 文件{ string, ArrayBuffer, TypedArray }
'module'加载 ES 模块{ string, ArrayBuffer, TypedArray }
'wasm'加载 WebAssembly 模块{ ArrayBuffer, TypedArray }

source 的值对于类型 'builtin' 被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。 source 的值对于类型 'commonjs' 被忽略,因为 CommonJS 模块加载器没有为 ES 模块加载器提供覆盖 CommonJS 模块返回值 的机制。 这个限制将来可能会被克服。

警告:ESM load 钩子和来自 CommonJS 模块的命名空间导出不兼容。 尝试将它们一起使用将导致导入中的空对象。 这可能会在未来得到解决。

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

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

load 钩子提供了一种方法来定义用于检索 ES 模块说明符的源代码的自定义方法。 这将允许加载器潜在地避免从磁盘读取文件。 它还可以用于将无法识别的格式映射到支持的格式,例如 yamlmodule

export async function load(url, context, nextLoad) {
  const { format } = context;

  if (Math.random() > 0.5) { // 某些条件
    /*
      For some or all URLs, do some custom logic for retrieving the source.
      Always return an object of the form {
        format: <string>,
        source: <string|buffer>,
      }.
    */
    return {
      format,
      shortCircuit: true,
      source: '...',
    };
  }

  // 推迟到链中的下一个钩子。
  return nextLoad(url);
}

在更高级的场景中,这也可用于将不受支持的源转换为受支持的源(请参阅下面的示例)。

globalPreload()#

中英对照

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

在此 API 的先前版本中,此钩子被命名为 getGlobalPreloadCode

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

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

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

export function globalPreload(context) {
  return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');

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

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

为了允许应用程序和加载程序之间进行通信,预加载代码中提供了另一个参数:port。 这可以作为加载器钩子的参数和钩子返回的源文本内部。 必须注意正确调用 port.ref()port.unref() 以防止进程处于无法正常关闭的状态。

/**
 * This example has the application context send a message to the loader
 * and sends the message back to the application context
 */
export function globalPreload({ port }) {
  port.onmessage = (evt) => {
    port.postMessage(evt.data);
  };
  return `\
    port.postMessage('console.log("I went to the Loader and back");');
    port.onmessage = (evt) => {
      eval(evt.data);
    };
  `;
}

示例#

中英对照

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

HTTPS 加载器#

中英对照

在当前的 Node.js 中,以 https:// 开头的说明符是实验性的(参见 HTTPS 和 HTTP 导入)。

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

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

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

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

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

export function load(url, context, nextLoad) {
  // 要通过网络加载 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({
          // 本示例假设所有网络提供的 JavaScript 
          // 都是 ES 模块代码。
          format: 'module',
          shortCircuit: true,
          source: data,
        }));
      }).on('error', (err) => reject(err));
    });
  }

  // 让 Node.js 处理所有其他 URL。
  return nextLoad(url);
}
// 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。

转译器加载器#

中英对照

可以使用 load 钩子将 Node.js 无法理解的格式的源代码转换为 JavaScript。 但是,在调用该钩子之前,resolve 钩子需要告诉 Node.js 不要在未知文件类型上抛出错误。

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

// coffeescript-loader.mjs
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import CoffeeScript from 'coffeescript';

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

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

export async function resolve(specifier, context, nextResolve) {
  if (extensionsRegex.test(specifier)) {
    const { parentURL = baseURL } = context;

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

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

export async function load(url, context, nextLoad) {
  if (extensionsRegex.test(url)) {
    // 现在我们修补了解析让 CoffeeScript URL 通过,
    // 我们需要告诉 Node.js 这些 URL 应该被解释为什么格式。因为
    // 因为 CoffeeScript 会转译成 JavaScript,
    // 所以它应该是两种 JavaScript 格式之一:'commonjs' 或 'module'。

    // CoffeeScript 文件可以是 CommonJS 或 ES 模块,
    // 因此我们希望 Node.js 将任何 CoffeeScript 文件视为相同位置的 .js 文件。 
    // 要确定 Node.js 如何解释任意 .js 文件,
    // 则在文件系统中搜索最近的父 package.json 文件 
    // 并读取其 "type" 字段。
    const format = await getPackageType(url);
    // 当钩子返回 'commonjs' 格式时,则 `source` 将被忽略。
    // 为了处理 CommonJS 文件,需要使用 `require.extensions` 注册句柄,
    // 以便使用 CommonJS 加载器处理文件。
    // 避免需要单独的 CommonJS 处理程序 
    // 是 ES 模块加载器计划的未来增强功能。
    if (format === 'commonjs') {
      return {
        format,
        shortCircuit: true,
      };
    }

    const { source: rawSource } = await nextLoad(url, { ...context, format });
    // 此钩子将所有导入的 CoffeeScript 文件的 CoffeeScript 源代码 
    // 转换为的 JavaScript 源代码。
    const transformedSource = coffeeCompile(rawSource.toString(), url);

    return {
      format,
      shortCircuit: true,
      source: transformedSource,
    };
  }

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

async function getPackageType(url) {
  // `url` is only a file path during the first iteration when passed the
  // resolved url from the load() hook
  // an actual file path from load() will contain a file extension as it's
  // required by the spec
  // this simple truthy check for whether `url` contains a file extension will
  // work for most projects but does not cover some edge-cases (such as
  // extensionless files or a url ending in a trailing space)
  const isFilePath = !!extname(url);
  // 如果是文件路径,则获取它所在的目录
  const dir = isFilePath ?
    dirname(fileURLToPath(url)) :
    url;
  // 生成同一个目录下的 package.json 的文件路径,
  // 文件可能存在也可能不存在
  const packagePath = resolvePath(dir, 'package.json');
  // 尝试读取可能不存在的 package.json
  const type = await readFile(packagePath, { encoding: 'utf8' })
    .then((filestring) => JSON.parse(filestring).type)
    .catch((err) => {
      if (err?.code !== 'ENOENT') console.error(err);
    });
  // 如果 package.json 存在并包含带有值的 `type` 字段
  if (type) return type;
  // 否则,(如果不在根目录下)继续检查下一个目录
  // 如果在根目录,则停止并返回 false
  return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
}
# main.coffee
import { scream } from './scream.coffee'
console.log scream 'hello, world'

import { version } from 'node: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 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. Let format be undefined.
  7. If resolved is a "file:" URL, then
    1. If resolved contains any percent encodings of "/" or "\" ("%2F" and "%5C" respectively), then
      1. Throw an Invalid Module Specifier error.
    2. If the file at resolved is a directory, then
      1. Throw an Unsupported Directory Import error.
    3. If the file at resolved does not exist, then
      1. Throw a Module Not Found error.
    4. Set resolved to the real path of resolved, maintaining the same URL querystring and fragment components.
    5. Set format to the result of ESM_FILE_FORMAT(resolved).
  8. Otherwise,
    1. Set format the module format of the content type associated with the URL resolved.
  9. Load resolved as module format, format.

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 is a Node.js builtin module name, then
    1. Return the string "node:" concatenated with packageSpecifier.
  4. 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.
  5. 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.
  6. If packageName starts with "." or contains "\" or "%", then
    1. Throw an Invalid Module Specifier error.
  7. Let packageSubpath be "." concatenated with the substring of packageSpecifier from the position at the length of packageName.
  8. If packageSubpath ends in "/", then
    1. Throw an Invalid Module Specifier error.
  9. Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
  10. If selfUrl is not undefined, return selfUrl.
  11. 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. 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. Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
    6. Otherwise, if packageSubpath is equal to ".", then
      1. If pjson.main is a string, then
        1. Return the URL resolution of main in packageURL.
    7. Otherwise,
      1. Return the URL resolution of packageSubpath in packageURL.
  12. Throw a Module Not Found error.

PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)

  1. Let packageURL be the result of LOOKUP_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 result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, 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, 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 resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
    3. If resolved is not null or undefined, return resolved.
  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 LOOKUP_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 resolved be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( specifier, pjson.imports, packageURL, true, conditions).
      2. If resolved is not null or undefined, return resolved.
  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 contain "*", then
    1. Let target be the value of matchObj[matchKey].
    2. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, "", false, isImports, conditions).
  2. Let expansionKeys be the list of keys of matchObj containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity.
  3. For each key expansionKey in expansionKeys, do
    1. Let patternBase be the substring of expansionKey up to but excluding the first "*" character.
    2. If matchKey starts with but is not equal to patternBase, then
      1. Let patternTrailer be the substring of expansionKey from the index after the first "*" character.
      2. If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of 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 patternBase up to the length of matchKey minus the length of patternTrailer.
        3. Return the result of PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, true, isImports, conditions).
  4. Return null.

PATTERN_KEY_COMPARE(keyA, keyB)

  1. Assert: keyA ends with "/" or contains only a single "*".
  2. Assert: keyB ends with "/" or contains only a single "*".
  3. Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise.
  4. Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise.
  5. If baseLengthA is greater than baseLengthB, return -1.
  6. If baseLengthB is greater than baseLengthA, return 1.
  7. If keyA does not contain "*", return 1.
  8. If keyB does not contain "*", return -1.
  9. If the length of keyA is greater than the length of keyB, return -1.
  10. If the length of keyB is greater than the length of keyA, return 1.
  11. Return 0.

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, case insensitive and including percent encoded variants, 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, case insensitive and including percent encoded variants, 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_FILE_FORMAT(url)

  1. Assert: url corresponds to an existing file.
  2. If url ends in ".mjs", then
    1. Return "module".
  3. If url ends in ".cjs", then
    1. Return "commonjs".
  4. If url ends in ".json", then
    1. Return "json".
  5. Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(url).
  6. Let pjson be the result of READ_PACKAGE_JSON(packageURL).
  7. 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.
  8. Otherwise,
    1. Throw an Unsupported File Extension error.

LOOKUP_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 pjsonURL be the resolution of "package.json" within scopeURL.
    4. if the file at pjsonURL exists, then
      1. Return scopeURL.
  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 - 实验

不要依赖此标志。我们计划在加载器 API 发展到可以通过自定义加载器实现等效功能时将其删除。

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

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

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