Node.js v24.13.0 文档


模块:node:module API#>

【Modules: node:module API】

Module 对象#>

【The Module object】

在与 Module 实例交互时提供通用实用方法,这是在 CommonJS 模块中经常看到的 module 变量。可以通过 import 'node:module'require('node:module') 访问。

【Provides general utility methods when interacting with instances of Module, the module variable often seen in CommonJS modules. Accessed via import 'node:module' or require('node:module').】

module.builtinModules#>

Node.js 提供的所有模块名称列表。可用于验证某个模块是否由第三方维护。

【A list of the names of all modules provided by Node.js. Can be used to verify if a module is maintained by a third party or not.】

在此上下文中,module 并不是 模块封装 提供的同一个对象。要访问它,需要引入 Module 模块:

// module.mjs
// In an ECMAScript module
import { builtinModules as builtin } from 'node:module';// module.cjs
// In a CommonJS module
const builtin = require('node:module').builtinModules;

module.createRequire(filename)#>

  • filename <string> | <URL> 用于构建 require 函数的文件名。必须是文件 URL 对象、文件 URL 字符串或绝对路径字符串。
  • 返回:<require> 需要函数
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

// sibling-module.js is a CommonJS module.
const siblingModule = require('./sibling-module'); 

module.findPackageJSON(specifier[, base])#>

稳定性: 1.1 - 处于活跃开发中

  • specifier <string> | <URL> 要检索其 package.json 的模块的指定符。传入 裸指定符 时,将返回包根目录下的 package.json。传入 相对指定符绝对指定符 时,将返回最近的父级 package.json
  • base <string> | <URL> 包含模块的绝对位置(file: URL 字符串或文件系统路径)。对于 CJS,使用 __filename(不要使用 __dirname!);对于 ESM,使用 import.meta.url。如果 specifier 是一个绝对指定符,则无需传递它。
  • 返回值:<string> | <undefined> 如果找到 package.json,则返回一个路径。当 specifier 是一个包时,返回该包的根 package.json;当是相对路径或未解析时,返回离 specifier 最近的 package.json

注意:不要用这个来尝试确定模块格式。有许多因素会影响这个判断;package.json 中的 type 字段是最不具决定性的(例如文件扩展名会覆盖它,而加载器钩子会进一步覆盖)。

注意:目前这仅使用内置的默认解析器;如果注册了 resolve 自定义钩子,它们将不会影响解析。 将来可能会有所改变。

/path/to/project
  ├ packages/
    ├ bar/
      ├ bar.js
      └ package.json // name = '@foo/bar'
    └ qux/
      ├ node_modules/
        └ some-package/
          └ package.json // name = 'some-package'
      ├ qux.js
      └ package.json // name = '@foo/qux'
  ├ main.js
  └ package.json // name = '@foo' 
// /path/to/project/packages/bar/bar.js
import { findPackageJSON } from 'node:module';

findPackageJSON('..', import.meta.url);
// '/path/to/project/package.json'
// Same result when passing an absolute specifier instead:
findPackageJSON(new URL('../', import.meta.url));
findPackageJSON(import.meta.resolve('../'));

findPackageJSON('some-package', import.meta.url);
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `package.json`.
findPackageJSON(import.meta.resolve('some-package'));
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'

findPackageJSON('@foo/qux', import.meta.url);
// '/path/to/project/packages/qux/package.json'// /path/to/project/packages/bar/bar.js
const { findPackageJSON } = require('node:module');
const { pathToFileURL } = require('node:url');
const path = require('node:path');

findPackageJSON('..', __filename);
// '/path/to/project/package.json'
// Same result when passing an absolute specifier instead:
findPackageJSON(pathToFileURL(path.join(__dirname, '..')));

findPackageJSON('some-package', __filename);
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `package.json`.
findPackageJSON(pathToFileURL(require.resolve('some-package')));
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'

findPackageJSON('@foo/qux', __filename);
// '/path/to/project/packages/qux/package.json'

module.isBuiltin(moduleName)#>

  • moduleName <string> 模块的名称
  • 返回值:<boolean> 如果模块是内置的则返回 true,否则返回 false
import { isBuiltin } from 'node:module';
isBuiltin('node:fs'); // true
isBuiltin('fs'); // true
isBuiltin('wss'); // false 

module.register(specifier[, parentURL][, options])#>

稳定性: 1.2 - 发布候选版

  • specifier <string> | <URL> 要注册的自定义钩子;这应该与传递给 import() 的字符串相同,除非它是相对路径,相对路径将相对于 parentURL 解析。
  • parentURL <string> | <URL> 如果你想要相对于一个基 URL(例如 import.meta.url)解析 specifier,你可以在这里传入该 URL。默认值: 'data:'
  • options <Object>
    • parentURL <string> | <URL> 如果你想基于某个基础 URL(例如 import.meta.url)解析 specifier,可以在此处传递该 URL。如果 parentURL 作为第二个参数提供,则会忽略此属性。默认值: 'data:'
    • data <any> 任何可任意克隆的 JavaScript 值,用于传入 initialize 钩子。
    • transferList <Object[]> 可转移对象 将传入 initialize 钩子。

注册一个模块,该模块导出 钩子,用于自定义 Node.js 模块的解析和加载行为。参见 自定义钩子

【Register a module that exports hooks that customize Node.js module resolution and loading behavior. See Customization hooks.】

如果与 权限模型 一起使用此功能,则需要 --allow-worker

【This feature requires --allow-worker if used with the Permission Model.】

module.registerHooks(options)#>

稳定性: 1.1 - 处于活跃开发中

注册 钩子,以自定义 Node.js 模块的解析和加载行为。请参见 自定义钩子

【Register hooks that customize Node.js module resolution and loading behavior. See Customization hooks.】

module.stripTypeScriptTypes(code[, options])#>

稳定性: 1.2 - 发布候选版

  • code <string> 要去除类型注解的代码。
  • options <Object>
    • mode <string> 默认值: 'strip'。可能的取值有:
      • 'strip' 只去除类型注解,不执行 TypeScript 特性的转换。
      • 'transform' 去除类型注解并将 TypeScript 特性转换为 JavaScript。
    • sourceMap <boolean> 默认值: false。仅当 mode'transform' 时,如果设置为 true,将为转换后的代码生成源映射。
    • sourceUrl <string> 指定在源映射中使用的源 URL。
  • 返回值:<string> 删除类型注解后的代码。module.stripTypeScriptTypes() 会从 TypeScript 代码中移除类型注解。它可以用于在使用 vm.runInContext()vm.compileFunction() 运行 TypeScript 代码之前去除类型注解。默认情况下,如果代码包含需要转换的 TypeScript 特性(如 Enums),会抛出错误,更多信息请参见 类型剥离。当 mode 为 'transform' 时,它还会将 TypeScript 特性转换为 JavaScript,更多信息请参见 转换 TypeScript 功能。当 mode 为 'strip' 时,不会生成源地图,因为位置被保留。如果提供了 sourceMap,在 mode 为 'strip' 时会抛出错误。

警告: 由于 TypeScript 解析器的变化,该函数的输出在不同的 Node.js 版本中不应被视为稳定。

WARNING: The output of this function should not be considered stable across Node.js versions, due to changes in the TypeScript parser.】

import { stripTypeScriptTypes } from 'node:module';
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a         = 1;const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code);
console.log(strippedCode);
// Prints: const a         = 1;

如果提供了 sourceUrl,它将作为注释附加在输出的末尾:

【If sourceUrl is provided, it will be used appended as a comment at the end of the output:】

import { stripTypeScriptTypes } from 'node:module';
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
console.log(strippedCode);
// Prints: const a         = 1\n\n//# sourceURL=source.ts;const { stripTypeScriptTypes } = require('node:module');
const code = 'const a: number = 1;';
const strippedCode = stripTypeScriptTypes(code, { mode: 'strip', sourceUrl: 'source.ts' });
console.log(strippedCode);
// Prints: const a         = 1\n\n//# sourceURL=source.ts;

mode'transform' 时,代码将被转换为 JavaScript:

【When mode is 'transform', the code is transformed to JavaScript:】

import { stripTypeScriptTypes } from 'node:module';
const code = `
  namespace MathUtil {
    export const add = (a: number, b: number) => a + b;
  }`;
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
console.log(strippedCode);
// Prints:
// var MathUtil;
// (function(MathUtil) {
//     MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...const { stripTypeScriptTypes } = require('node:module');
const code = `
  namespace MathUtil {
    export const add = (a: number, b: number) => a + b;
  }`;
const strippedCode = stripTypeScriptTypes(code, { mode: 'transform', sourceMap: true });
console.log(strippedCode);
// Prints:
// var MathUtil;
// (function(MathUtil) {
//     MathUtil.add = (a, b)=>a + b;
// })(MathUtil || (MathUtil = {}));
// # sourceMappingURL=data:application/json;base64, ...

module.syncBuiltinESMExports()#>

module.syncBuiltinESMExports() 方法会更新内置 ES 模块 的所有实时绑定,以匹配 CommonJS 导出的属性。它不会添加或删除 ES 模块 的导出名称。

【The module.syncBuiltinESMExports() method updates all the live bindings for builtin ES Modules to match the properties of the CommonJS exports. It does not add or remove exported names from the ES Modules.】

const fs = require('node:fs');
const assert = require('node:assert');
const { syncBuiltinESMExports } = require('node:module');

fs.readFile = newAPI;

delete fs.readFileSync;

function newAPI() {
  // ...
}

fs.newAPI = newAPI;

syncBuiltinESMExports();

import('node:fs').then((esmFS) => {
  // It syncs the existing readFile property with the new value
  assert.strictEqual(esmFS.readFile, newAPI);
  // readFileSync has been deleted from the required fs
  assert.strictEqual('readFileSync' in fs, false);
  // syncBuiltinESMExports() does not remove readFileSync from esmFS
  assert.strictEqual('readFileSync' in esmFS, true);
  // syncBuiltinESMExports() does not add names
  assert.strictEqual(esmFS.newAPI, undefined);
}); 

模块编译缓存#>

【Module compile cache】

模块编译缓存可以通过 module.enableCompileCache() 方法或 NODE_COMPILE_CACHE=dir 环境变量启用。启用后,每当 Node.js 编译 CommonJS、ECMAScript 模块或 TypeScript 模块时,它将使用指定目录中持久化在磁盘上的 V8 代码缓存 来加速编译。这可能会减慢模块图的首次加载速度,但如果模块内容没有变化,后续加载同一模块图时可能会获得显著的加速。

【The module compile cache can be enabled either using the module.enableCompileCache() method or the NODE_COMPILE_CACHE=dir environment variable. After it is enabled, whenever Node.js compiles a CommonJS, a ECMAScript Module, or a TypeScript module, it will use on-disk V8 code cache persisted in the specified directory to speed up the compilation. This may slow down the first load of a module graph, but subsequent loads of the same module graph may get a significant speedup if the contents of the modules do not change.】

要清理磁盘上生成的编译缓存,只需删除缓存目录。下次在相同目录中用于编译缓存存储时,该缓存目录将会被重新创建。为避免磁盘被过时的缓存占满,建议使用 os.tmpdir() 下的目录。如果通过调用 module.enableCompileCache() 启用编译缓存而未指定 directory,Node.js 将使用 NODE_COMPILE_CACHE=dir 环境变量(如果已设置),否则默认使用 path.join(os.tmpdir(), 'node-compile-cache')。要查找正在运行的 Node.js 实例使用的编译缓存目录,请使用 module.getCompileCacheDir()

【To clean up the generated compile cache on disk, simply remove the cache directory. The cache directory will be recreated the next time the same directory is used for for compile cache storage. To avoid filling up the disk with stale cache, it is recommended to use a directory under the os.tmpdir(). If the compile cache is enabled by a call to module.enableCompileCache() without specifying the directory, Node.js will use the NODE_COMPILE_CACHE=dir environment variable if it's set, or defaults to path.join(os.tmpdir(), 'node-compile-cache') otherwise. To locate the compile cache directory used by a running Node.js instance, use module.getCompileCacheDir().】

启用的模块编译缓存可以通过 NODE_DISABLE_COMPILE_CACHE=1 环境变量禁用。当编译缓存导致意外或不希望出现的行为(例如测试覆盖率不够精确)时,这可能会很有用。

【The enabled module compile cache can be disabled by the NODE_DISABLE_COMPILE_CACHE=1 environment variable. This can be useful when the compile cache leads to unexpected or undesired behaviors (e.g. less precise test coverage).】

目前,当启用编译缓存并重新加载模块时,代码缓存会立即从已编译的代码生成,但只有在 Node.js 实例即将退出时才会写入磁盘。这可能会有所变化。可以使用 module.flushCompileCache() 方法来确保已累积的代码缓存被刷新到磁盘,以防应用希望生成其他 Node.js 实例,并让它们在父进程退出很久之前就共享缓存。

【At the moment, when the compile cache is enabled and a module is loaded afresh, the code cache is generated from the compiled code immediately, but will only be written to disk when the Node.js instance is about to exit. This is subject to change. The module.flushCompileCache() method can be used to ensure the accumulated code cache is flushed to disk in case the application wants to spawn other Node.js instances and let them share the cache long before the parent exits.】

编译缓存的可移植性#>

【Portability of the compile cache】

默认情况下,当被缓存模块的绝对路径发生变化时,缓存会失效。为了在移动项目目录后仍然保持缓存有效,可以启用可移植编译缓存。这样,只要相对于缓存目录的布局保持不变,以前编译的模块就可以在不同的目录位置重复使用。这是基于最佳努力的方式进行的。如果 Node.js 无法计算模块相对于缓存目录的位置,该模块将不会被缓存。

【By default, caches are invalidated when the absolute paths of the modules being cached are changed. To keep the cache working after moving the project directory, enable portable compile cache. This allows previously compiled modules to be reused across different directory locations as long as the layout relative to the cache directory remains the same. This would be done on a best-effort basis. If Node.js cannot compute the location of a module relative to the cache directory, the module will not be cached.】

有两种方法可以启用便携模式:

【There are two ways to enable the portable mode:】

  1. module.enableCompileCache() 中使用便携选项:

    // Non-portable cache (default): cache breaks if project is moved
    module.enableCompileCache({ directory: '/path/to/cache/storage/dir' });
    
    // Portable cache: cache works after the project is moved
    module.enableCompileCache({ directory: '/path/to/cache/storage/dir', portable: true }); 
  2. 设置环境变量:NODE_COMPILE_CACHE_PORTABLE=1

编译缓存的局限性#>

【Limitations of the compile cache】

目前,当在 V8 JavaScript 代码覆盖率 中使用编译缓存时,V8 收集的覆盖率在从代码缓存反序列化的函数中可能不够精确。建议在运行测试以生成精确覆盖率时关闭此功能。

【Currently when using the compile cache with V8 JavaScript code coverage, the coverage being collected by V8 may be less precise in functions that are deserialized from the code cache. It's recommended to turn this off when running tests to generate precise coverage.】

由一个版本的 Node.js 生成的编译缓存不能被不同版本的 Node.js 重用。如果使用相同的基础目录来保存缓存,不同版本的 Node.js 生成的缓存将会被单独存储,因此它们可以共存。

【Compilation cache generated by one version of Node.js can not be reused by a different version of Node.js. Cache generated by different versions of Node.js will be stored separately if the same base directory is used to persist the cache, so they can co-exist.】

module.constants.compileCacheStatus#>

稳定性: 1.1 - 处于活跃开发中

以下常量作为对象中返回的 status 字段返回,由 module.enableCompileCache() 用于指示尝试启用 模块编译缓存 的结果。

【The following constants are returned as the status field in the object returned by module.enableCompileCache() to indicate the result of the attempt to enable the module compile cache.】

Constant Description
ENABLED Node.js has enabled the compile cache successfully. The directory used to store the compile cache will be returned in the directory field in the returned object.
ALREADY_ENABLED The compile cache has already been enabled before, either by a previous call to module.enableCompileCache(), or by the NODE_COMPILE_CACHE=dir environment variable. The directory used to store the compile cache will be returned in the directory field in the returned object.
FAILED Node.js fails to enable the compile cache. This can be caused by the lack of permission to use the specified directory, or various kinds of file system errors. The detail of the failure will be returned in the message field in the returned object.
DISABLED Node.js cannot enable the compile cache because the environment variable NODE_DISABLE_COMPILE_CACHE=1 has been set.

module.enableCompileCache([options])#>

稳定性: 1.1 - 处于活跃开发中

  • options <string> | <Object> 可选。如果传入字符串,则视为 options.directory
    • directory <string> 可选。用于存储编译缓存的目录。如果未指定,将使用 NODE_COMPILE_CACHE=dir 环境变量指定的目录(如果已设置),否则使用 path.join(os.tmpdir(), 'node-compile-cache')
    • portable <boolean> 可选。如果为 true,则启用可移植编译缓存,使缓存即使在项目目录被移动后也可以重用。这是一个尽力而为的功能。如果未指定,则取决于环境变量 NODE_COMPILE_CACHE_PORTABLE=1 是否已设置。
  • 返回:<Object>
    • status <integer> module.constants.compileCacheStatus之一
    • message <string> | <undefined> 如果 Node.js 无法启用编译缓存,则此处包含错误信息。只有在 statusmodule.constants.compileCacheStatus.FAILED 时才会设置。
    • directory <string> | <undefined> 如果启用了编译缓存,本字段包含存储编译缓存的目录。仅在 statusmodule.constants.compileCacheStatus.ENABLEDmodule.constants.compileCacheStatus.ALREADY_ENABLED 时设置。

在当前的 Node.js 实例中启用 模块编译缓存

【Enable module compile cache in the current Node.js instance.】

对于一般使用情况,建议调用 module.enableCompileCache() 时不要指定 options.directory,这样在必要时可以通过 NODE_COMPILE_CACHE 环境变量覆盖该目录。

【For general use cases, it's recommended to call module.enableCompileCache() without specifying the options.directory, so that the directory can be overridden by the NODE_COMPILE_CACHE environment variable when necessary.】

由于编译缓存被认为是一种非关键任务的优化,因此当无法启用编译缓存时,该方法设计为不会抛出任何异常。相反,它将返回一个对象,其中 message 字段包含错误信息以便调试。如果编译缓存成功启用,返回对象中的 directory 字段将包含存储编译缓存的目录路径。返回对象中的 status 字段将是 module.constants.compileCacheStatus 的某个值,以指示尝试启用 模块编译缓存 的结果。

【Since compile cache is supposed to be a optimization that is not mission critical, this method is designed to not throw any exception when the compile cache cannot be enabled. Instead, it will return an object containing an error message in the message field to aid debugging. If compile cache is enabled successfully, the directory field in the returned object contains the path to the directory where the compile cache is stored. The status field in the returned object would be one of the module.constants.compileCacheStatus values to indicate the result of the attempt to enable the module compile cache.】

此方法仅影响当前的 Node.js 实例。要在子工作线程中启用它,你可以在子工作线程中也调用此方法,或者将 process.env.NODE_COMPILE_CACHE 设置为编译缓存目录,这样该行为就可以继承到子工作线程中。该目录可以通过此方法返回的 directory 字段获取,或者使用 module.getCompileCacheDir() 获取。

【This method only affects the current Node.js instance. To enable it in child worker threads, either call this method in child worker threads too, or set the process.env.NODE_COMPILE_CACHE value to compile cache directory so the behavior can be inherited into the child workers. The directory can be obtained either from the directory field returned by this method, or with module.getCompileCacheDir().】

module.flushCompileCache()#>

稳定性: 1.1 - 处于活跃开发中

将当前 Node.js 实例中已加载模块产生的 模块编译缓存 刷新到磁盘。无论文件系统操作是否成功完成,本操作都会在所有刷新操作完成后返回。如果有任何错误,该操作将默默失败,因为编译缓存未命中不应影响应用的实际运行。

【Flush the module compile cache accumulated from modules already loaded in the current Node.js instance to disk. This returns after all the flushing file system operations come to an end, no matter they succeed or not. If there are any errors, this will fail silently, since compile cache misses should not interfere with the actual operation of the application.】

module.getCompileCacheDir()#>

稳定性: 1.1 - 处于活跃开发中

定制钩子#>

【Customization Hooks】

稳定性: 1.1 - 处于活跃开发中

目前支持两种类型的模块自定义钩子:

【There are two types of module customization hooks that are currently supported:】

  1. module.register(specifier[,父级网址][, options]) 接收一个导出异步钩子函数的模块。这些函数将在一个单独的加载线程上运行。
  2. module.registerHooks(options) 接受同步钩子函数,这些函数会在模块加载的线程上直接运行。

启用#>

【Enabling】

模块解析和加载可以通过以下方式自定义:

【Module resolution and loading can be customized by:】

  1. 注册一个导出一组异步钩子函数的文件,使用来自 node:moduleregister 方法,
  2. 使用 node:module 中的 registerHooks 方法注册一组同步钩子函数。

可以在应用代码运行之前使用 --import--require 标志注册这些钩子:

【The hooks can be registered before the application code is run by using the --import or --require flag:】

node --import ./register-hooks.js ./my-app.js
node --require ./register-hooks.js ./my-app.js 
// register-hooks.js
// This file can only be require()-ed if it doesn't contain top-level await.
// Use module.register() to register asynchronous hooks in a dedicated thread.
import { register } from 'node:module';
register('./hooks.mjs', import.meta.url);// register-hooks.js
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
// Use module.register() to register asynchronous hooks in a dedicated thread.
register('./hooks.mjs', pathToFileURL(__filename));
// Use module.registerHooks() to register synchronous hooks in the main thread.
import { registerHooks } from 'node:module';
registerHooks({
  resolve(specifier, context, nextResolve) { /* implementation */ },
  load(url, context, nextLoad) { /* implementation */ },
});// Use module.registerHooks() to register synchronous hooks in the main thread.
const { registerHooks } = require('node:module');
registerHooks({
  resolve(specifier, context, nextResolve) { /* implementation */ },
  load(url, context, nextLoad) { /* implementation */ },
});

传递给 --import--require 的文件也可以是某个依赖的导出内容:

【The file passed to --import or --require can also be an export from a dependency:】

node --import some-package/register ./my-app.js
node --require some-package/register ./my-app.js 

some-package 包含一个 "exports" 字段,用于定义 /register 导出,以映射到调用 register() 的文件,例如以下 register-hooks.js 示例。

【Where some-package has an "exports" field defining the /register export to map to a file that calls register(), like the following register-hooks.js example.】

使用 --import--require 可以确保在导入任何应用文件之前注册钩子,包括应用的入口点以及默认情况下的任何工作线程。

【Using --import or --require ensures that the hooks are registered before any application files are imported, including the entry point of the application and for any worker threads by default as well.】

或者,可以从入口点调用 register()registerHooks(),不过对于任何应在钩子注册后运行的 ESM 代码,必须使用动态 import()

【Alternatively, register() and registerHooks() can be called from the entry point, though dynamic import() must be used for any ESM code that should be run after the hooks are registered.】

import { register } from 'node:module';

register('http-to-https', import.meta.url);

// Because this is a dynamic `import()`, the `http-to-https` hooks will run
// to handle `./my-app.js` and any other files it imports or requires.
await import('./my-app.js');const { register } = require('node:module');
const { pathToFileURL } = require('node:url');

register('http-to-https', pathToFileURL(__filename));

// Because this is a dynamic `import()`, the `http-to-https` hooks will run
// to handle `./my-app.js` and any other files it imports or requires.
import('./my-app.js');

自定义钩子将在注册后加载的任何模块以及它们通过 import 和内置 require 引用的模块上运行。通过 module.createRequire() 创建的用户 require 函数只能通过同步钩子进行自定义。

【Customization hooks will run for any modules loaded later than the registration and the modules they reference via import and the built-in require. require function created by users using module.createRequire() can only be customized by the synchronous hooks.】

在此示例中,我们正在注册 http-to-https 钩子,但它们仅对随后导入的模块可用——在本例中是 my-app.js 以及它通过 import 或 CommonJS 依赖中的内置 require 引用的任何内容。

【In this example, we are registering the http-to-https hooks, but they will only be available for subsequently imported modules — in this case, my-app.js and anything it references via import or built-in require in CommonJS dependencies.】

如果 import('./my-app.js') 改为静态的 import './my-app.js',应用将在注册 http-to-https 钩子之前就已经被加载。这是由于 ES 模块规范,静态导入会先从依赖树的叶子节点开始求值,然后再回到主干。在 my-app.js 内部可能还有静态导入,这些导入只有在动态导入 my-app.js 时才会被求值。

【If the import('./my-app.js') had instead been a static import './my-app.js', the app would have already been loaded before the http-to-https hooks were registered. This due to the ES modules specification, where static imports are evaluated from the leaves of the tree first, then back to the trunk. There can be static imports within my-app.js, which will not be evaluated until my-app.js is dynamically imported.】

如果使用同步钩子,则同时支持 importrequire 以及使用 createRequire() 创建的用户 require

【If synchronous hooks are used, both import, require and user require created using createRequire() are supported.】

import { registerHooks, createRequire } from 'node:module';

registerHooks({ /* implementation of synchronous hooks */ });

const require = createRequire(import.meta.url);

// The synchronous hooks affect import, require() and user require() function
// created through createRequire().
await import('./my-app.js');
require('./my-app-2.js');const { register, registerHooks } = require('node:module');
const { pathToFileURL } = require('node:url');

registerHooks({ /* implementation of synchronous hooks */ });

const userRequire = createRequire(__filename);

// The synchronous hooks affect import, require() and user require() function
// created through createRequire().
import('./my-app.js');
require('./my-app-2.js');
userRequire('./my-app-3.js');

最后,如果你只是想在应用运行前注册钩子,并且不想为此创建一个单独的文件,你可以将一个 data: URL 传递给 --import

【Finally, if all you want to do is register hooks before your app runs and you don't want to create a separate file for that purpose, you can pass a data: URL to --import:】

node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("http-to-https", pathToFileURL("./"));' ./my-app.js 

链接#>

【Chaining】

可以多次调用 register

【It's possible to call register more than once:】

// entrypoint.mjs
import { register } from 'node:module';

register('./foo.mjs', import.meta.url);
register('./bar.mjs', import.meta.url);
await import('./my-app.mjs');// entrypoint.cjs
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');

const parentURL = pathToFileURL(__filename);
register('./foo.mjs', parentURL);
register('./bar.mjs', parentURL);
import('./my-app.mjs');

在这个例子中,注册的钩子将形成链。这些链以后进先出(LIFO)的顺序运行。如果 foo.mjsbar.mjs 都定义了 resolve 钩子,它们将按如下方式调用(注意从右到左): node 默认 ← ./foo.mjs./bar.mjs (从 ./bar.mjs 开始,然后是 ./foo.mjs,最后是 Node.js 默认)。 其他所有钩子也同样适用。

【In this example, the registered hooks will form chains. These chains run last-in, first out (LIFO). If both foo.mjs and bar.mjs define a resolve hook, they will be called like so (note the right-to-left): node's default ← ./foo.mjs./bar.mjs (starting with ./bar.mjs, then ./foo.mjs, then the Node.js default). The same applies to all the other hooks.】

已注册的钩子也会影响 register 本身。在这个例子中,bar.mjs 将通过 foo.mjs 注册的钩子来解析和加载(因为 foo 的钩子已经被添加到链中)。这允许像使用非 JavaScript 语言编写钩子这样的操作,只要先前注册的钩子能够转译成 JavaScript。

【The registered hooks also affect register itself. In this example, bar.mjs will be resolved and loaded via the hooks registered by foo.mjs (because foo's hooks will have already been added to the chain). This allows for things like writing hooks in non-JavaScript languages, so long as earlier registered hooks transpile into JavaScript.】

register 方法不能在定义钩子的模块内部调用。

【The register method cannot be called from within the module that defines the hooks.】

registerHooks 的链式调用工作方式类似。如果同步和异步钩子混用,同步钩子总是会在异步钩子开始运行之前先执行,也就是说,在最后一个同步钩子运行时,它的下一个钩子会包含对异步钩子的调用。

【Chaining of registerHooks work similarly. If synchronous and asynchronous hooks are mixed, the synchronous hooks are always run first before the asynchronous hooks start running, that is, in the last synchronous hook being run, its next hook includes invocation of the asynchronous hooks.】

// entrypoint.mjs
import { registerHooks } from 'node:module';

const hook1 = { /* implementation of hooks */ };
const hook2 = { /* implementation of hooks */ };
// hook2 run before hook1.
registerHooks(hook1);
registerHooks(hook2);// entrypoint.cjs
const { registerHooks } = require('node:module');

const hook1 = { /* implementation of hooks */ };
const hook2 = { /* implementation of hooks */ };
// hook2 run before hook1.
registerHooks(hook1);
registerHooks(hook2);

与模块定制钩子通信#>

【Communication with module customization hooks】

异步钩子在专用线程上运行,与运行应用代码的主线程分开。这意味着修改全局变量不会影响其他线程,并且必须使用消息通道在线程之间进行通信。

【Asynchronous hooks run on a dedicated thread, separate from the main thread that runs application code. This means mutating global variables won't affect the other thread(s), and message channels must be used to communicate between the threads.】

register 方法可用于向 initialize 钩子传递数据。传递给钩子的数据可能包括可传输的对象,例如端口。

【The register method can be used to pass data to an initialize hook. The data passed to the hook may include transferable objects like ports.】

import { register } from 'node:module';
import { MessageChannel } from 'node:worker_threads';

// This example demonstrates how a message channel can be used to
// communicate with the hooks, by sending `port2` to the hooks.
const { port1, port2 } = new MessageChannel();

port1.on('message', (msg) => {
  console.log(msg);
});
port1.unref();

register('./my-hooks.mjs', {
  parentURL: import.meta.url,
  data: { number: 1, port: port2 },
  transferList: [port2],
});const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');

// This example showcases how a message channel can be used to
// communicate with the hooks, by sending `port2` to the hooks.
const { port1, port2 } = new MessageChannel();

port1.on('message', (msg) => {
  console.log(msg);
});
port1.unref();

register('./my-hooks.mjs', {
  parentURL: pathToFileURL(__filename),
  data: { number: 1, port: port2 },
  transferList: [port2],
});

同步模块钩子在运行应用代码的同一线程上运行。它们可以直接修改主线程访问的上下文的全局变量。

【Synchronous module hooks are run on the same thread where the application code is run. They can directly mutate the globals of the context accessed by the main thread.】

钩子#>

【Hooks】

module.register() 接受异步钩子#>

【Asynchronous hooks accepted by module.register()

register 方法可用于注册导出一组钩子的模块。钩子是由 Node.js 调用的函数,用于自定义模块解析和加载过程。导出的函数必须具有特定的名称和签名,并且必须以命名导出的形式导出。

【The register method can be used to register a module that exports a set of hooks. The hooks are functions that are called by Node.js to customize the module resolution and loading process. The exported functions must have specific names and signatures, and they must be exported as named exports.】

export async function initialize({ number, port }) {
  // Receives data from `register`.
}

export async function resolve(specifier, context, nextResolve) {
  // Take an `import` or `require` specifier and resolve it to a URL.
}

export async function load(url, context, nextLoad) {
  // Take a resolved URL and return the source code to be evaluated.
} 

异步钩子在一个独立的线程中运行,与执行应用代码的主线程隔离。这意味着它是一个不同的字段。钩子线程可能会随时被主线程终止,因此不要依赖异步操作(如 console.log)的完成。它们默认会继承到子工作线程中。

【Asynchronous hooks are run in a separate thread, isolated from the main thread where application code runs. That means it is a different realm. The hooks thread may be terminated by the main thread at any time, so do not depend on asynchronous operations (like console.log) to complete. They are inherited into child workers by default.】

module.registerHooks() 接受同步钩子#>

【Synchronous hooks accepted by module.registerHooks()

稳定性: 1.1 - 处于活跃开发中

module.registerHooks() 方法接受同步钩子函数。initialize() 不被支持也不必要,因为钩子实现者可以在调用 module.registerHooks() 之前直接运行初始化代码。

【The module.registerHooks() method accepts synchronous hook functions. initialize() is not supported nor necessary, as the hook implementer can simply run the initialization code directly before the call to module.registerHooks().】

function resolve(specifier, context, nextResolve) {
  // Take an `import` or `require` specifier and resolve it to a URL.
}

function load(url, context, nextLoad) {
  // Take a resolved URL and return the source code to be evaluated.
} 

同步钩子在相同的线程和加载模块的相同 字段 中运行。与异步钩子不同,它们默认不会继承到子工作线程中,不过如果钩子是通过 --import--require 预加载的文件注册的,子工作线程可以通过 process.execArgv 继承预加载的脚本。详情见 Worker 的文档

【Synchronous hooks are run in the same thread and the same realm where the modules are loaded. Unlike the asynchronous hooks they are not inherited into child worker threads by default, though if the hooks are registered using a file preloaded by --import or --require, child worker threads can inherit the preloaded scripts via process.execArgv inheritance. See the documentation of Worker for detail.】

在同步钩子中,用户可以预期 console.log() 的执行方式与他们在模块代码中预期 console.log() 的执行方式相同。

【In synchronous hooks, users can expect console.log() to complete in the same way that they expect console.log() in module code to complete.】

钩子的约定#>

【Conventions of hooks】

Hooks 是 链条 的一部分,即使该链只包含一个自定义(用户提供的)hook 和始终存在的默认 hook。Hook 函数是嵌套的:每个函数必须总是返回一个普通对象,而链式调用的发生是因为每个函数调用了 next<hookName>(),它是对后续加载器的 hook 的引用(按后进先出顺序)。

返回缺少必需属性的值的钩子会触发异常。一个钩子如果没有调用 next<hookName>() 并且也没有返回 shortCircuit: true,同样会触发异常。这些错误是为了帮助防止链条意外中断。从钩子返回 shortCircuit: true 表示链条在你的钩子处故意结束。

initialize()#>

稳定性: 1.2 - 发布候选版

  • data <any> 来自 register(loader, import.meta.url, { data }) 的数据。

initialize 钩子仅被 register 接受。registerHooks() 不支持也不需要它,因为同步钩子的初始化可以在调用 registerHooks() 之前直接执行。

【The initialize hook is only accepted by register. registerHooks() does not support nor need it since initialization done for synchronous hooks can be run directly before the call to registerHooks().】

initialize 钩子提供了一种方式,可以定义一个自定义函数,当 hooks 模块初始化时,该函数将在 hooks 线程中运行。当通过 register 注册 hooks 模块时,初始化就会发生。

【The initialize hook provides a way to define a custom function that runs in the hooks thread when the hooks module is initialized. Initialization happens when the hooks module is registered via register.】

这个钩子可以从 register 调用接收数据,包括端口和其他可传递对象。initialize 的返回值可以是一个 <Promise>,在这种情况下,它将在主应用线程执行恢复之前被等待。

【This hook can receive data from a register invocation, including ports and other transferable objects. The return value of initialize can be a <Promise>, in which case it will be awaited before the main application thread execution resumes.】

模块定制代码:

【Module customization code:】

// path-to-my-hooks.js

export async function initialize({ number, port }) {
  port.postMessage(`increment: ${number + 1}`);
} 

调用者代码:

【Caller code:】

import assert from 'node:assert';
import { register } from 'node:module';
import { MessageChannel } from 'node:worker_threads';

// This example showcases how a message channel can be used to communicate
// between the main (application) thread and the hooks running on the hooks
// thread, by sending `port2` to the `initialize` hook.
const { port1, port2 } = new MessageChannel();

port1.on('message', (msg) => {
  assert.strictEqual(msg, 'increment: 2');
});
port1.unref();

register('./path-to-my-hooks.js', {
  parentURL: import.meta.url,
  data: { number: 1, port: port2 },
  transferList: [port2],
});const assert = require('node:assert');
const { register } = require('node:module');
const { pathToFileURL } = require('node:url');
const { MessageChannel } = require('node:worker_threads');

// This example showcases how a message channel can be used to communicate
// between the main (application) thread and the hooks running on the hooks
// thread, by sending `port2` to the `initialize` hook.
const { port1, port2 } = new MessageChannel();

port1.on('message', (msg) => {
  assert.strictEqual(msg, 'increment: 2');
});
port1.unref();

register('./path-to-my-hooks.js', {
  parentURL: pathToFileURL(__filename),
  data: { number: 1, port: port2 },
  transferList: [port2],
});
resolve(specifier, context, nextResolve)#>
  • specifier <string>
  • context <Object>
    • conditions <string[]> 导出相关 package.json 的条件
    • importAttributes <Object> 一个对象,其键值对表示要导入模块的属性
    • parentURL <string> | <undefined> 导入此模块的模块,或者如果这是 Node.js 的入口点,则为 undefined
  • nextResolve <Function> 链中后续的 resolve 钩子,或在最后一个用户提供的 resolve 钩子之后的 Node.js 默认 resolve 钩子
    • specifier <string>
    • context <Object> | <undefined> 如果省略,则使用默认值。提供时,默认值会与提供的属性合并,并优先使用提供的属性。
  • 返回值:<Object> | <Promise> 异步版本可以接受包含以下属性的对象,或者一个将解析为该对象的 Promise。同步版本只接受同步返回的对象。
    • format <string> | <null> | <undefined>load 钩子的一个提示(可能会被忽略)。它可以是一个模块格式(例如 'commonjs''module'),也可以是任意值,如 'css''yaml'
    • importAttributes <Object> | <undefined> 缓存模块时使用的导入属性(可选;如果省略,则使用输入)
    • shortCircuit <undefined> | <boolean> 一个信号,表示此钩子打算终止 resolve 钩子的链条。默认值: false
    • url <string> 此输入解析后的绝对 URL

警告 在异步版本中,尽管支持返回 promise 和异步函数,调用 resolve 仍可能阻塞主线程,从而影响性能。

resolve 钩子链负责告诉 Node.js 在哪里查找以及如何缓存给定的 import 语句或表达式,或 require 调用。它可以选择性地返回一个格式(例如 'module')作为 load 钩子的提示。如果指定了格式,load 钩子最终负责提供最终的 format 值(并且可以忽略 resolve 提供的提示);如果 resolve 提供了 format,即使只是为了将该值传递给 Node.js 的默认 load 钩子,也需要自定义 load 钩子。

【The resolve hook chain is responsible for telling Node.js where to find and how to cache a given import statement or expression, or require call. It can optionally return a format (such as 'module') as a hint to the load hook. If a format is specified, the load hook is ultimately responsible for providing the final format value (and it is free to ignore the hint provided by resolve); if resolve provides a format, a custom load hook is required even if only to pass the value to the Node.js default load hook.】

导入类型属性是将加载的模块保存到内部模块缓存中的缓存键的一部分。如果模块应以不同于源代码中存在的属性进行缓存,resolve 钩子负责返回一个 importAttributes 对象。

【Import type attributes are part of the cache key for saving loaded modules into the internal module cache. The resolve hook is responsible for returning an importAttributes object if the module should be cached with different attributes than were present in the source code.】

context 中的 conditions 属性是一个条件数组,用于匹配此解析请求的 包导出条件。它们可以用于在其他地方查找条件映射,或在调用默认解析逻辑时修改列表。

【The conditions property in context is an array of conditions that will be used to match package exports conditions for this resolution request. They can be used for looking up conditional mappings elsewhere or to modify the list when calling the default resolution logic.】

当前的 包导出条件 总是包含在传入 hook 的 context.conditions 数组中。为了在调用 defaultResolve 时保证 默认的 Node.js 模块指定符解析行为,传递给它的 context.conditions 数组 必须 包含最初传入 resolve hook 的 context.conditions 数组的 所有 元素。

【The current package exports conditions are always in the context.conditions array passed into the hook. To guarantee default Node.js module specifier resolution behavior when calling defaultResolve, the context.conditions array passed to it must include all elements of the context.conditions array originally passed into the resolve hook.】

// Asynchronous version accepted by module.register().
export async function resolve(specifier, context, nextResolve) {
  const { parentURL = null } = context;

  if (Math.random() > 0.5) { // Some condition.
    // For some or all specifiers, do some custom logic for resolving.
    // Always return an object of the form {url: <string>}.
    return {
      shortCircuit: true,
      url: parentURL ?
        new URL(specifier, parentURL).href :
        new URL(specifier).href,
    };
  }

  if (Math.random() < 0.5) { // Another condition.
    // When calling `defaultResolve`, the arguments can be modified. In this
    // case it's adding another value for matching conditional exports.
    return nextResolve(specifier, {
      ...context,
      conditions: [...context.conditions, 'another-condition'],
    });
  }

  // Defer to the next hook in the chain, which would be the
  // Node.js default resolve if this is the last user-specified loader.
  return nextResolve(specifier);
} 
// Synchronous version accepted by module.registerHooks().
function resolve(specifier, context, nextResolve) {
  // Similar to the asynchronous resolve() above, since that one does not have
  // any asynchronous logic.
} 
load(url, context, nextLoad)#>
  • url <string> resolve 链返回的 URL
  • context <Object>
    • conditions <string[]> 导出相关 package.json 的条件
    • format <string> | <null> | <undefined>resolve 钩子链可选提供的格式。输入可以是任何字符串值;输入值不需要符合下面描述的可接受返回值列表。
    • importAttributes <Object>
  • nextLoad <Function> 链中后续的 load 钩子,或在最后一个用户提供的 load 钩子之后的 Node.js 默认 load 钩子
    • url <string>

    • context <Object> | <undefined> 如果省略,将提供默认值。提供时,默认值会与提供的属性合并,并以提供的属性为优先。在默认的 nextLoad 中,如果 url 指向的模块没有明确的模块类型信息,则 context.format 是必需的。

  • 返回值: <Object> | <Promise> 异步版本接收一个包含以下属性的对象,或者一个将解析为该对象的 Promise。 同步版本仅接受同步返回的对象。

load 钩子提供了一种定义自定义方法的方法,用于确定 URL 应该如何被解释、获取和解析。它还负责验证导入属性。

【The load hook provides a way to define a custom method of determining how a URL should be interpreted, retrieved, and parsed. It is also in charge of validating the import attributes.】

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

【The final value of format must be one of the following:】

formatDescriptionAcceptable types for source returned by load
'addon'Load a Node.js addon<null>
'builtin'Load a Node.js builtin module<null>
'commonjs-typescript'Load a Node.js CommonJS module with TypeScript syntax<string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined>
'commonjs'Load a Node.js CommonJS module<string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined>
'json'Load a JSON file<string> | <ArrayBuffer> | <TypedArray>
'module-typescript'Load an ES module with TypeScript syntax<string> | <ArrayBuffer> | <TypedArray>
'module'Load an ES module<string> | <ArrayBuffer> | <TypedArray>
'wasm'Load a WebAssembly module<ArrayBuffer> | <TypedArray>

source 的值会被忽略,因为对于 'builtin' 类型,目前无法替换 Node.js 内置(核心)模块的值。

【The value of source is ignored for type 'builtin' because currently it is not possible to replace the value of a Node.js builtin (core) module.】

在异步 load 钩子中的注意事项#>

【Caveat in the asynchronous load hook】

在使用异步 load 钩子时,为 'commonjs' 省略或提供 source 会产生非常不同的效果:

【When using the asynchronous load hook, omitting vs providing a source for 'commonjs' has very different effects:】

  • 当提供 source 时,该模块中的所有 require 调用将由 ESM 加载器处理,并使用已注册的 resolveload 钩子;该模块中的所有 require.resolve 调用将由 ESM 加载器处理,并使用已注册的 resolve 钩子;只有部分 CommonJS API 可用(例如没有 require.extensions、没有 require.cache、没有 require.resolve.paths),对 CommonJS 模块加载器的猴子补丁将不生效。
  • 如果 source 是未定义或 null,它将由 CommonJS 模块加载器处理,并且 require/require.resolve 调用不会经过注册的钩子。对于 null 值的 source,这种行为是暂时的——将来将不再支持 null 值的 source

这些注意事项不适用于同步的 load 钩子,在这种情况下,自定义 CommonJS 模块可以使用完整的 CommonJS API 集,require/require.resolve 始终会通过已注册的钩子。

【These caveats do not apply to the synchronous load hook, in which case the complete set of CommonJS APIs available to the customized CommonJS modules, and require/require.resolve always go through the registered hooks.】

Node.js 内部的异步 load 实现,即 load 链中最后一个钩子的 next 值,当 format'commonjs' 时,会为了向后兼容而返回 null 作为 source。下面是一个示例钩子,它将选择使用非默认行为:

【The Node.js internal asynchronous load implementation, which is the value of next for the last hook in the load chain, returns null for source when format is 'commonjs' for backward compatibility. Here is an example hook that would opt-in to using the non-default behavior:】

import { readFile } from 'node:fs/promises';

// Asynchronous version accepted by module.register(). This fix is not needed
// for the synchronous version accepted by module.registerHooks().
export async function load(url, context, nextLoad) {
  const result = await nextLoad(url, context);
  if (result.format === 'commonjs') {
    result.source ??= await readFile(new URL(result.responseURL ?? url));
  }
  return result;
} 

这同样不适用于同步的 load 钩子,在这种情况下,返回的 source 包含由下一个钩子加载的源代码,无论模块格式如何。

【This doesn't apply to the synchronous load hook either, in which case the source returned contains source code loaded by the next hook, regardless of module format.】

警告:异步 load 钩子与来自 CommonJS 模块的命名空间导出不兼容。尝试将它们一起使用将导致导入结果为空对象。未来可能会解决此问题。这不适用于同步 load 钩子,在这种情况下可以像平常一样使用导出。

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

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

【If the source value of a text-based format (i.e., 'json', 'module') is not a string, it is converted to a string using util.TextDecoder.】

load 钩子提供了一种方式来定义用于获取已解析 URL 源代码的自定义方法。这允许加载器在潜在情况下避免从磁盘读取文件。它也可以用于将无法识别的格式映射为受支持的格式,例如将 yaml 映射为 module

【The load hook provides a way to define a custom method for retrieving the source code of a resolved URL. This would allow a loader to potentially avoid reading files from disk. It could also be used to map an unrecognized format to a supported one, for example yaml to module.】

// Asynchronous version accepted by module.register().
export async function load(url, context, nextLoad) {
  const { format } = context;

  if (Math.random() > 0.5) { // Some condition
    /*
      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: '...',
    };
  }

  // Defer to the next hook in the chain.
  return nextLoad(url);
} 
// Synchronous version accepted by module.registerHooks().
function load(url, context, nextLoad) {
  // Similar to the asynchronous load() above, since that one does not have
  // any asynchronous logic.
} 

在更高级的场景中,这也可以用来将不受支持的源转换为受支持的源(见下方示例)。

【In a more advanced scenario, this can also be used to transform an unsupported source to a supported one (see Examples below).】

示例#>

【Examples】

各种模块自定义钩子可以结合使用,以实现对 Node.js 代码加载和执行行为的广泛定制。

【The various module customization hooks can be used together to accomplish wide-ranging customizations of the Node.js code loading and evaluation behaviors.】

从 HTTPS 导入#>

【Import from HTTPS】

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

【The hook below registers hooks to enable rudimentary support for such specifiers. While this may seem like a significant improvement to Node.js core functionality, there are substantial downsides to actually using these hooks: performance is much slower than loading files from disk, there is no caching, and there is no security.】

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

export function load(url, context, nextLoad) {
  // For JavaScript to be loaded over the network, we need to fetch and
  // return it.
  if (url.startsWith('https://')) {
    return new Promise((resolve, reject) => {
      get(url, (res) => {
        let data = '';
        res.setEncoding('utf8');
        res.on('data', (chunk) => data += chunk);
        res.on('end', () => resolve({
          // This example assumes all network-provided JavaScript is ES module
          // code.
          format: 'module',
          shortCircuit: true,
          source: data,
        }));
      }).on('error', (err) => reject(err));
    });
  }

  // Let Node.js handle all other URLs.
  return nextLoad(url);
} 
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';

console.log(VERSION); 

使用前面的 hooks 模块,运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs 会打印 main.mjs 中 URL 模块的当前 CoffeeScript 版本。

【With the preceding hooks module, running node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs prints the current version of CoffeeScript per the module at the URL in main.mjs.】

转换#>

【Transpilation】

Node.js 无法理解的格式的源文件可以使用 load 钩子 转换为 JavaScript。

【Sources that are in formats Node.js doesn't understand can be converted into JavaScript using the load hook.】

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

【This is less performant than transpiling source files before running Node.js; transpiler hooks should only be used for development and testing purposes.】

异步版本#>

【Asynchronous version】

// coffeescript-hooks.mjs
import { readFile } from 'node:fs/promises';
import { findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;

export async function load(url, context, nextLoad) {
  if (extensionsRegex.test(url)) {
    // CoffeeScript files can be either CommonJS or ES modules. Use a custom format
    // to tell Node.js not to detect its module type.
    const { source: rawSource } = await nextLoad(url, { ...context, format: 'coffee' });
    // This hook converts CoffeeScript source code into JavaScript source code
    // for all imported CoffeeScript files.
    const transformedSource = coffeescript.compile(rawSource.toString(), url);

    // To determine how Node.js would interpret the transpilation result,
    // search up the file system for the nearest parent package.json file
    // and read its "type" field.
    return {
      format: await getPackageType(url),
      shortCircuit: true,
      source: transformedSource,
    };
  }

  // Let Node.js handle all other URLs.
  return nextLoad(url, context);
}

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 pJson = findPackageJSON(url);

  return readFile(pJson, 'utf8')
    .then(JSON.parse)
    .then((json) => json?.type)
    .catch(() => undefined);
} 

同步版本#>

【Synchronous version】

// coffeescript-sync-hooks.mjs
import { readFileSync } from 'node:fs';
import { registerHooks, findPackageJSON } from 'node:module';
import coffeescript from 'coffeescript';

const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;

function load(url, context, nextLoad) {
  if (extensionsRegex.test(url)) {
    const { source: rawSource } = nextLoad(url, { ...context, format: 'coffee' });
    const transformedSource = coffeescript.compile(rawSource.toString(), url);

    return {
      format: getPackageType(url),
      shortCircuit: true,
      source: transformedSource,
    };
  }

  return nextLoad(url, context);
}

function getPackageType(url) {
  const pJson = findPackageJSON(url);
  if (!pJson) {
    return undefined;
  }
  try {
    const file = readFileSync(pJson, 'utf-8');
    return JSON.parse(file)?.type;
  } catch {
    return undefined;
  }
}

registerHooks({ load }); 

正在运行的钩子#>

【Running hooks】

# 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() 

为了运行示例,添加一个包含 CoffeeScript 文件模块类型的 package.json 文件。

【For the sake of running the example, add a package.json file containing the module type of the CoffeeScript files.】

{
  "type": "module"
} 

这仅用于运行示例。在实际的加载器中,即使在 package.json 中没有显式的类型,getPackageType() 也必须能够返回 Node.js 已知的 format,否则 nextLoad 调用将抛出 ERR_UNKNOWN_FILE_EXTENSION(如果未定义)或 ERR_UNKNOWN_MODULE_FORMAT(如果它不是 加载钩子 文档中列出的已知格式)。

【This is only for running the example. In real world loaders, getPackageType() must be able to return an format known to Node.js even in the absence of an explicit type in a package.json, or otherwise the nextLoad call would throw ERR_UNKNOWN_FILE_EXTENSION (if undefined) or ERR_UNKNOWN_MODULE_FORMAT (if it's not a known format listed in the load hook documentation).】

使用前面的钩子模块,运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee 或者 node --import ./coffeescript-sync-hooks.mjs ./main.coffee 会在从磁盘加载 main.coffee 的源代码后,但在 Node.js 执行之前,将其转换为 JavaScript;对于通过 import 语句引用的任何已加载文件的 .coffee.litcoffee.coffee.md 文件也同样适用。

【With the preceding hooks modules, running node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee or node --import ./coffeescript-sync-hooks.mjs ./main.coffee causes main.coffee to be turned into JavaScript after its source code is loaded from disk but before Node.js executes it; and so on for any .coffee, .litcoffee or .coffee.md files referenced via import statements of any loaded file.】

导入映射#>

【Import maps】

前两个例子定义了 load 钩子。下面是一个 resolve 钩子的例子。这个钩子模块会读取一个 import-map.json 文件,该文件定义了哪些标识符需要重写为其他 URL(这是对“导入映射”规范中一个小子集的非常简单的实现)。

【The previous two examples defined load hooks. This is an example of a resolve hook. This hooks module reads an import-map.json file that defines which specifiers to override to other URLs (this is a very simplistic implementation of a small subset of the "import maps" specification).】

异步版本#>

【Asynchronous version】

// import-map-hooks.js
import fs from 'node:fs/promises';

const { imports } = JSON.parse(await fs.readFile('import-map.json'));

export async function resolve(specifier, context, nextResolve) {
  if (Object.hasOwn(imports, specifier)) {
    return nextResolve(imports[specifier], context);
  }

  return nextResolve(specifier, context);
} 

同步版本#>

【Synchronous version】

// import-map-sync-hooks.js
import fs from 'node:fs/promises';
import module from 'node:module';

const { imports } = JSON.parse(fs.readFileSync('import-map.json', 'utf-8'));

function resolve(specifier, context, nextResolve) {
  if (Object.hasOwn(imports, specifier)) {
    return nextResolve(imports[specifier], context);
  }

  return nextResolve(specifier, context);
}

module.registerHooks({ resolve }); 

使用钩子#>

【Using the hooks】

有了这些文件:

【With these files:】

// main.js
import 'a-module'; 
// import-map.json
{
  "imports": {
    "a-module": "./some-module.js"
  }
} 
// some-module.js
console.log('some module!'); 

运行 node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./import-map-hooks.js"));' main.jsnode --import ./import-map-sync-hooks.js main.js 应该会打印 some module!

【Running node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./import-map-hooks.js"));' main.js or node --import ./import-map-sync-hooks.js main.js should print some module!.】

源映射支持#>

【Source Map Support】

稳定性: 1 - 实验性

Node.js 支持 TC39 ECMA-426 源映射 格式(它被称为源映射第 3 版格式)。

【Node.js supports TC39 ECMA-426 Source Map format (it was called Source map revision 3 format).】

本节中的 API 是用于与源映射缓存交互的辅助工具。当启用源映射解析并且在模块的页脚中找到 源映射包含指令 时,会填充此缓存。

【The APIs in this section are helpers for interacting with the source map cache. This cache is populated when source map parsing is enabled and source map include directives are found in a modules' footer.】

要启用源映射解析,Node.js 必须使用标志 --enable-source-maps 运行,或者通过设置 NODE_V8_COVERAGE=dir 启用代码覆盖,或者通过 module.setSourceMapsSupport() 编程方式启用。

【To enable source map parsing, Node.js must be run with the flag --enable-source-maps, or with code coverage enabled by setting NODE_V8_COVERAGE=dir, or be enabled programmatically via module.setSourceMapsSupport().】

// module.mjs
// In an ECMAScript module
import { findSourceMap, SourceMap } from 'node:module';// module.cjs
// In a CommonJS module
const { findSourceMap, SourceMap } = require('node:module');

module.getSourceMapsSupport()#>

  • 返回:<Object>
    • enabled <boolean> 如果启用了源映射支持
    • nodeModules <boolean> 如果启用了对 node_modules 中文件的支持。
    • generatedCode <boolean> 如果启用了对来自 evalnew Function 的生成代码的支持。

此方法返回 源映射 v3 堆栈跟踪支持是否已启用。

【This method returns whether the Source Map v3 support for stack traces is enabled.】

module.findSourceMap(path)#>

path 是文件的解析路径,应为该文件获取相应的源映射。

module.setSourceMapsSupport(enabled[, options])#>

  • enabled <boolean> 启用源映射支持。
  • options <Object> 可选
    • nodeModules <boolean> 如果启用对 node_modules 中文件的支持。默认值: false
    • generatedCode <boolean> 如果启用对来自 evalnew Function 的生成代码的支持。默认值: false

此功能用于启用或禁用对堆栈跟踪的 源映射 v3 支持。

【This function enables or disables the Source Map v3 support for stack traces.】

它提供与使用命令行选项 --enable-source-maps 启动 Node.js 进程相同的功能,同时增加了用于修改对 node_modules 中的文件或生成的代码支持的附加选项。

【It provides same features as launching Node.js process with commandline options --enable-source-maps, with additional options to alter the support for files in node_modules or generated codes.】

只有在启用源映射后加载的 JavaScript 文件中的源映射才会被解析和加载。最好使用命令行选项 --enable-source-maps,以避免在调用此 API 之前加载的模块的源映射丢失。

【Only source maps in JavaScript files that are loaded after source maps has been enabled will be parsed and loaded. Preferably, use the commandline options --enable-source-maps to avoid losing track of source maps of modules loaded before this API call.】

类:module.SourceMap#>

【Class: module.SourceMap

new SourceMap(payload[, { lineLengths }])#>

创建一个新的 sourceMap 实例。

【Creates a new sourceMap instance.】

payload 是一个键与 源映射格式 匹配的对象:

lineLengths 是一个可选数组,用于表示生成代码中每一行的长度。

sourceMap.payload#>

用于构造 SourceMap 实例的有效负载的获取方法。

【Getter for the payload used to construct the SourceMap instance.】

sourceMap.findEntry(lineOffset, columnOffset)#>
  • lineOffset <number> 生成源中的零索引行号偏移
  • columnOffset <number> 生成源中的零索引列号偏移
  • 返回:<Object>

给定生成源文件中的行偏移和列偏移,如果找到,则返回表示原始文件中 SourceMap 范围的对象;如果未找到,则返回空对象。

【Given a line offset and column offset in the generated source file, returns an object representing the SourceMap range in the original file if found, or an empty object if not.】

返回的对象包含以下键:

【The object returned contains the following keys:】

  • generatedLine <number> 生成源中范围起始行的行偏移
  • generatedColumn <number> 在生成源中范围开始的列偏移量
  • originalSource <string> 原始来源的文件名,如 SourceMap 中所示
  • originalLine <number> 原始源中范围起始行的行偏移
  • originalColumn <number> 原始源中范围起始的列偏移
  • name <string>

返回值表示在 SourceMap 中出现的原始范围,基于零索引偏移,而不是在错误信息和 CallSite 对象中显示的从 1 开始的行号和列号。

【The returned value represents the raw range as it appears in the SourceMap, based on zero-indexed offsets, not 1-indexed line and column numbers as they appear in Error messages and CallSite objects.】

要从 Error 堆栈和 CallSite 对象报告的 lineNumber 和 columnNumber 获取对应的从 1 开始的行号和列号,请使用 sourceMap.findOrigin(lineNumber, columnNumber)

【To get the corresponding 1-indexed line and column numbers from a lineNumber and columnNumber as they are reported by Error stacks and CallSite objects, use sourceMap.findOrigin(lineNumber, columnNumber)

sourceMap.findOrigin(lineNumber, columnNumber)#>
  • lineNumber <number> 生成源代码中调用位置的1起始索引行号
  • columnNumber <number> 生成源代码中调用位置的从1开始的列号
  • 返回:<Object>

给定生成源代码中调用位置的从 1 开始索引的 lineNumbercolumnNumber,找到原始源代码中对应的调用位置。

【Given a 1-indexed lineNumber and columnNumber from a call site in the generated source, find the corresponding call site location in the original source.】

如果提供的 lineNumbercolumnNumber 在任何源映射中都未找到,则返回一个空对象。否则,返回的对象包含以下键:

【If the lineNumber and columnNumber provided are not found in any source map, then an empty object is returned. Otherwise, the returned object contains the following keys:】

  • name <string> | <undefined> 在源映射中范围的名称(如果提供了的话)
  • fileName <string> 原始来源的文件名,如 SourceMap 中所报告的
  • lineNumber <number> 原始代码中对应调用位置的 1 索引行号
  • columnNumber <number> 原始源代码中对应调用位置的以1为起始的列号
Node.js 中文网 - 粤ICP备13048890号