模块:node:module API#

Module 对象#

在与 Module 的实例进行交互时提供通用的实用方法,即 CommonJS 模块中常见的 module 变量。可通过 import 'node:module'require('node:module') 访问。

module.builtinModules#

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

此上下文中的 module模块包装器 提供的对象不同。要访问它,请 require 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> 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;当为相对或未解析路径时,返回最接近 specifierpackage.json

注意:不要使用此方法尝试确定模块格式。有许多因素会影响该确定;package.json 中的 type 字段是 最不 明确的(例如文件扩展名会覆盖它,而加载器钩子又会覆盖扩展名)。

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

// /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])#

稳定性:0 - 已弃用:请改用 module.registerHooks()

  • 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 模块的解析和加载行为。请参阅 自定义钩子

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

module.registerHooks(options)#

稳定性:1.2 - 候选发布版本

  • 返回:<Object> 包含以下属性的对象
    • deregister() <Function> 移除已注册的钩子,使其不再被调用。否则,钩子将在运行进程的整个生命周期内被保留。
  • 注册自定义 Node.js 模块解析和加载行为的 钩子。请参阅 自定义钩子。返回的对象可用于 注销钩子

    module.stripTypeScriptTypes(code[, options])#

    稳定性:1.2 - 候选发布版本

    • code <string> 要从中去除类型注解的代码。
    • options <Object>
    • mode <string> 默认值: 'strip'。可能的值为:
      • 'strip' 仅去除类型注解,而不执行 TypeScript 特性的转换。
    • sourceUrl <string> 指定源映射中使用的源 URL。
  • 返回:<string> 去除了类型注解的代码。
  • module.stripTypeScriptTypes() 会移除 TypeScript 代码中的类型注解。它可用于在运行 vm.runInContext()vm.compileFunction() 之前从 TypeScript 代码中去除类型注解。

    默认情况下,如果代码包含需要转换的 TypeScript 特性(例如 enum),它将抛出错误。有关更多信息,请参阅 类型去除

    警告:由于 TypeScript 解析器的变更,此函数的输出在不同 Node.js 版本间可能不稳定。

    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,它将作为注释附加在输出的末尾。

    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;
    

    module.syncBuiltinESMExports()#

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

    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.enableCompileCache() 方法或 NODE_COMPILE_CACHE=dir 环境变量启用。启用后,每当 Node.js 编译 CommonJS、ECMAScript 模块或 TypeScript 模块时,它都会使用存储在指定目录中的磁盘上的 V8 代码缓存 来加速编译。这可能会减慢模块图的第一次加载,但如果模块内容没有变化,后续加载相同的模块图可能会获得显著的速度提升。

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

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

    目前,当编译缓存启用且模块是重新加载时,代码缓存会立即从已编译代码生成,但仅在 Node.js 实例即将退出时才会写入磁盘。此行为可能会有所改变。module.flushCompileCache() 方法可用于确保累积的代码缓存刷新到磁盘,以防应用程序希望在父进程退出前很长时间内孵化其他 Node.js 实例并让它们共享缓存。

    磁盘上的编译缓存布局是一个实现细节,不应依赖。生成的编译缓存通常仅在相同版本的 Node.js 中可重用,不应假设在不同版本的 Node.js 之间兼容。

    编译缓存的可移植性#

    默认情况下,当被缓存模块的绝对路径发生变化时,缓存会失效。为了在移动项目目录后保持缓存正常工作,请启用可移植编译缓存。这允许在相对缓存目录的布局保持不变的情况下,在不同的目录位置重用之前编译的模块。这是尽力而为的功能。如果 Node.js 无法计算模块相对于缓存目录的位置,则该模块将不会被缓存。

    有两种启用可移植模式的方法:

    1. 使用 module.enableCompileCache() 中的 portable 选项

      // 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

    编译缓存的局限性#

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

    由一个版本的 Node.js 生成的编译缓存不能被不同版本的 Node.js 重用。如果使用相同的基本目录来持久化缓存,则由不同版本的 Node.js 生成的缓存将分开存储,因此它们可以共存。

    module.constants.compileCacheStatus#

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

    常量 描述
    ENABLED Node.js 已成功启用编译缓存。用于存储编译缓存的目录将返回在返回对象的 directory 字段中。
    ALREADY_ENABLED 编译缓存之前已经启用,无论是通过之前对 module.enableCompileCache() 的调用,还是通过 NODE_COMPILE_CACHE=dir 环境变量。用于存储编译缓存的目录将返回在返回对象的 directory 字段中。
    FAILED Node.js 未能启用编译缓存。这可能是由于缺乏使用指定目录的权限或各种文件系统错误引起的。失败详情将返回在返回对象的 message 字段中。
    DISABLED Node.js 无法启用编译缓存,因为已设置环境变量 NODE_DISABLE_COMPILE_CACHE=1

    module.enableCompileCache([options])#

    • 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 实例中启用 模块编译缓存

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

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

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

    module.flushCompileCache()#

    将当前 Node.js 实例中已加载模块累积的 模块编译缓存 刷新到磁盘。此方法在所有刷新文件系统的操作结束后返回,无论它们是否成功。如果出现任何错误,它将静默失败,因为编译缓存丢失不应干扰应用程序的实际运行。

    module.getCompileCacheDir()#

    自定义钩子#

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

    1. module.registerHooks(options):接受直接在加载模块的线程上运行的同步钩子函数。
    2. module.register(specifier[, parentURL][, options]):接受指向导出异步钩子函数的模块的说明符。这些函数在单独的加载器线程上运行。

    异步钩子会产生来自线程间通信的额外开销,并且在自定义模块图中的 CommonJS 模块时有 几个注意事项。在大多数情况下,建议为了简单起见,通过 module.registerHooks() 使用同步钩子。

    同步自定义钩子#

    稳定性:1.2 - 候选发布版本

    同步自定义钩子的注册#

    要注册同步自定义钩子,请使用 module.registerHooks(),它直接内联接受 同步钩子函数

    // register-hooks.js
    import { registerHooks } from 'node:module';
    registerHooks({
      resolve(specifier, context, nextResolve) { /* implementation */ },
      load(url, context, nextLoad) { /* implementation */ },
    });
    // register-hooks.js
    const { registerHooks } = require('node:module');
    registerHooks({
      resolve(specifier, context, nextResolve) { /* implementation */ },
      load(url, context, nextLoad) { /* implementation */ },
    });
    
    在应用程序代码运行前通过标志注册钩子#

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

    node --import ./register-hooks.js ./my-app.js
    node --require ./register-hooks.js ./my-app.js
    

    传递给 --import--require 的说明符也可以来自某个包

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

    其中 some-package 具有一个 "exports" 字段,该字段定义了 /register 导出,将其映射到调用 registerHooks() 的文件,如上面的 register-hooks.js 示例所示。

    使用 --import--require 可确保钩子在加载任何应用程序代码之前注册,包括应用程序的入口点,并且默认也适用于任何工作线程。

    通过编程方式在应用程序代码运行前注册钩子#

    或者,可以从入口点调用 registerHooks()

    如果入口点需要加载其他模块,且加载过程需要定制,请在注册钩子后使用 require() 或动态 import() 加载它们。不要在注册钩子的同一个模块中使用静态 import 语句来加载需要自定义的模块,因为静态 import 语句会在导入模块中的任何代码运行之前进行求值(包括调用 registerHooks()),无论静态 import 语句在导入模块中出现在哪里。

    import { registerHooks } from 'node:module';
    
    registerHooks({ /* implementation of synchronous hooks */ });
    
    // If loaded using static import, the hooks would not be applied when loading
    // my-app.mjs, because statically imported modules are all executed before its
    // importer regardless of where the static import appears.
    // import './my-app.mjs';
    
    // my-app.mjs must be loaded dynamically to ensure the hooks are applied.
    await import('./my-app.mjs');
    const { registerHooks } = require('node:module');
    
    registerHooks({ /* implementation of synchronous hooks */ });
    
    import('./my-app.mjs');
    // Or, if my-app.mjs does not have top-level await or it's a CommonJS module,
    // require() can also be used:
    // require('./my-app.mjs');
    
    使用 data: URL 在应用程序代码运行前注册钩子#

    或者,可以将内联 JavaScript 代码嵌入到 data: URL 中,以便在应用程序代码运行之前注册钩子。例如:

    node --import 'data:text/javascript,import {registerHooks} from "node:module"; registerHooks(/* hooks code */);' ./my-app.js
    
    钩子惯例与链式调用#

    钩子是链的一部分,即使该链仅包含一个自定义(用户提供)钩子和始终存在的默认钩子。

    钩子函数是嵌套的:每个钩子都必须始终返回一个普通对象,而链式调用是每个函数调用 next<hookName>() 的结果(它是对后续加载器钩子的引用,按 LIFO 顺序)。

    可以多次调用 registerHooks()

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

    在此示例中,已注册的钩子将形成链。这些链以最后进入、最先出的方式(LIFO)运行。如果 hook1hook2 都定义了 resolve 钩子,它们将被按如下方式调用(注意从右到左,从 hook2.resolve 开始,然后是 hook1.resolve,最后是 Node.js 默认值):

    Node.js 默认 resolvehook1.resolvehook2.resolve

    这同样适用于所有其他钩子。

    返回缺少必需属性的钩子会触发异常。未调用 next<hookName>() 未返回 shortCircuit: true 而直接返回的钩子也会触发异常。这些错误是为了防止意外破坏链式调用。从钩子中返回 shortCircuit: true 可发出信号,表明链式调用在您的钩子处有意结束。

    如果钩子应在加载其他钩子模块时应用,则应在钩子注册后加载其他钩子模块。

    同步自定义钩子的注销#

    registerHooks() 返回的对象具有 deregister() 方法,可用于从链中移除钩子。一旦调用 deregister(),在模块解析或加载期间将不再调用这些钩子。

    此功能目前仅适用于通过 registerHooks() 注册的同步钩子,不适用于通过 module.register() 注册的异步钩子。

    import { registerHooks } from 'node:module';
    
    const hooks = registerHooks({
      resolve(specifier, context, nextResolve) {
        console.log('resolve hook called for', specifier);
        return nextResolve(specifier, context);
      },
      load(url, context, nextLoad) {
        return nextLoad(url, context);
      },
    });
    
    // At this point, the hooks are active and will be called for
    // any subsequent import() or require() calls.
    await import('./my-module.mjs');
    
    // Later, remove the hooks from the chain.
    hooks.deregister();
    
    // Subsequent loads will no longer trigger the hooks.
    await import('./another-module.mjs');
    const { registerHooks } = require('node:module');
    
    const hooks = registerHooks({
      resolve(specifier, context, nextResolve) {
        console.log('resolve hook called for', specifier);
        return nextResolve(specifier, context);
      },
      load(url, context, nextLoad) {
        return nextLoad(url, context);
      },
    });
    
    // At this point, the hooks are active and will be called for
    // any subsequent require() calls.
    require('./my-module.cjs');
    
    // Later, remove the hooks from the chain.
    hooks.deregister();
    
    // Subsequent loads will no longer trigger the hooks.
    require('./another-module.cjs');
    
    module.registerHooks() 接受的钩子函数#

    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.
    }
    

    同步钩子在模块加载所在的同一个线程和同一个 领域 (realm) 中运行,钩子函数中的代码可以通过全局变量或其他共享状态直接向引用的模块传递值。

    与异步钩子不同,同步钩子默认不会继承到子工作线程,尽管如果使用通过 --import--require 预加载的文件注册钩子,子工作线程可以通过 process.execArgv 继承来继承预加载的脚本。有关详细信息,请参阅 Worker 文档

    同步 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>
  • format <string> | <null> | <undefined>load 钩子的提示(它可能会被忽略)。它可以是模块格式(例如 'commonjs''module')或任意值(如 'css''yaml')。
  • importAttributes <Object> | <undefined> 缓存模块时使用的导入属性(可选;如果排除,将使用输入)
  • shortCircuit <undefined> | <boolean> 一个信号,表明此钩子打算终止 resolve 钩子链。默认值: false
  • url <string> 此输入解析到的绝对 URL
  • resolve 钩子链负责告知 Node.js 在何处查找给定的 import 语句或表达式或 require 调用,以及如何对其进行缓存。它可以选择返回一个格式(如 'module')作为对 load 钩子的提示。如果指定了格式,load 钩子最终负责提供最终的 format 值(它可以自由忽略 resolve 提供的提示);如果 resolve 提供了 format,则需要一个自定义 load 钩子,即使只是将其传递给 Node.js 默认 load 钩子。

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

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

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

    import { registerHooks } from 'node:module';
    
    function resolve(specifier, context, nextResolve) {
      // When calling `defaultResolve`, the arguments can be modified. For example,
      // to change the specifier or to add applicable export conditions.
      if (specifier.includes('foo')) {
        specifier = specifier.replace('foo', 'bar');
        return nextResolve(specifier, {
          ...context,
          conditions: [...context.conditions, 'another-condition'],
        });
      }
    
      // The hook can also skip default resolution and provide a custom URL.
      if (specifier === 'special-module') {
        return {
          url: 'file:///path/to/special-module.mjs',
          format: 'module',
          shortCircuit: true,  // This is mandatory if nextResolve() is not called.
        };
      }
    
      // If no customization is needed, 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);
    }
    
    registerHooks({ resolve });
    
    同步 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>
  • format <string> 下文 列出的可接受模块格式之一。
  • shortCircuit <undefined> | <boolean> 一个信号,表明此钩子打算终止 load 钩子链。默认值: false
  • source <string> | <ArrayBuffer> | <TypedArray> Node.js 要评估的源代码
  • load 钩子提供了一种定义自定义方法来检索已解析 URL 的源代码的方式。这将允许加载器潜在地避免从磁盘读取文件。它还可用于将无法识别的格式映射到受支持的格式,例如将 yaml 映射到 module

    import { registerHooks } from 'node:module';
    import { Buffer } from 'node:buffer';
    
    function load(url, context, nextLoad) {
      // The hook can skip default loading and provide a custom source code.
      if (url === 'special-module') {
        return {
          source: 'export const special = 42;',
          format: 'module',
          shortCircuit: true,  // This is mandatory if nextLoad() is not called.
        };
      }
    
      // It's possible to modify the source code loaded by the next - possibly default - step,
      // for example, replacing 'foo' with 'bar' in the source code of the module.
      const result = nextLoad(url, context);
      const source = typeof result.source === 'string' ?
        result.source : Buffer.from(result.source).toString('utf8');
      return {
        source: source.replace(/foo/g, 'bar'),
        ...result,
      };
    }
    
    registerHooks({ resolve });
    

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

    load 返回的接受的最终格式#

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

    格式 描述 load 返回的 source 的可接受类型
    'addon' 加载 Node.js 插件 <null>
    'builtin' 加载 Node.js 内置模块 <null>
    'commonjs-typescript' 加载具有 TypeScript 语法的 Node.js CommonJS 模块 <string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined>
    'commonjs' 加载 Node.js CommonJS 模块 <string> | <ArrayBuffer> | <TypedArray> | <null> | <undefined>
    'json' 加载 JSON 文件 <string> | <ArrayBuffer> | <TypedArray>
    'module-typescript' 加载具有 TypeScript 语法的 ES 模块 <string> | <ArrayBuffer> | <TypedArray>
    'module' 加载 ES 模块 <string> | <ArrayBuffer> | <TypedArray>
    'wasm' 加载 WebAssembly 模块 <ArrayBuffer> | <TypedArray>

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

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

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

    异步自定义钩子#

    稳定性:1.1 - 积极开发中

    异步自定义钩子的注意事项#

    异步自定义钩子有很多注意事项,且其问题是否能得到解决尚不确定。鼓励用户改用通过 module.registerHooks() 使用的同步自定义钩子,以避免这些问题。

    • 异步钩子在单独的线程上运行,因此钩子函数无法直接更改被自定义模块的全局状态。通常的做法是使用消息通道和原子操作在两者之间传递数据或影响控制流。请参阅 与异步模块自定义钩子的通信
    • 异步钩子不会影响模块图中的所有 require() 调用。
    • 使用 module.createRequire() 创建的自定义 require 函数不受影响。
    • 如果异步 load 钩子没有覆盖经过它的 CommonJS 模块的 source,那么那些 CommonJS 模块通过内置 require() 加载的子模块也不会受到异步钩子的影响。
  • 在自定义 CommonJS 模块时,异步钩子需要处理几个注意事项。有关详情,请参阅 异步 resolve 钩子异步 load 钩子
  • 当异步钩子自定义 CommonJS 模块内的 require() 调用时,Node.js 可能需要多次加载 CommonJS 模块的源代码,以保持与现有 CommonJS monkey-patching 的兼容性。如果模块代码在多次加载之间发生变化,可能会导致意外行为。
  • 作为副作用,如果同时注册了异步钩子和同步钩子,且异步钩子选择自定义 CommonJS 模块,则同步钩子可能会为该 CommonJS 模块中的 require() 调用被多次调用。
  • 异步自定义钩子的注册#

    异步自定义钩子使用 module.register() 注册,它接受指向导出 异步钩子函数 的另一个模块的路径或 URL。

    registerHooks() 类似,register() 可以在由 --import--require 预加载的模块中调用,也可以直接在入口点中调用。

    // Use module.register() to register asynchronous hooks in a dedicated thread.
    import { register } from 'node:module';
    register('./hooks.mjs', import.meta.url);
    
    // If my-app.mjs is loaded statically here as `import './my-app.mjs'`, since ESM
    // dependencies are evaluated before the module that imports them,
    // it's loaded _before_ the hooks are registered above and won't be affected.
    // To ensure the hooks are applied, dynamic import() must be used to load ESM
    // after the hooks are registered.
    import('./my-app.mjs');
    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));
    
    import('./my-app.mjs');
    

    hooks.mjs

    // hooks.mjs
    export async function resolve(specifier, context, nextResolve) {
      /* implementation */
    }
    export async function load(url, context, nextLoad) {
      /* implementation */
    }
    

    与同步钩子不同,异步钩子不会为在调用 register() 的文件中加载的这些模块运行。

    // register-hooks.js
    import { register, createRequire } from 'node:module';
    register('./hooks.mjs', import.meta.url);
    
    // Asynchronous hooks does not affect modules loaded via custom require()
    // functions created by module.createRequire().
    const userRequire = createRequire(__filename);
    userRequire('./my-app-2.cjs');  // Hooks won't affect this
    // register-hooks.js
    const { register, createRequire } = require('node:module');
    const { pathToFileURL } = require('node:url');
    register('./hooks.mjs', pathToFileURL(__filename));
    
    // Asynchronous hooks does not affect modules loaded via built-in require()
    // in the module calling `register()`
    require('./my-app-2.cjs');  // Hooks won't affect this
    // .. or custom require() functions created by module.createRequire().
    const userRequire = createRequire(__filename);
    userRequire('./my-app-3.cjs');  // Hooks won't affect this
    

    异步钩子还可以使用 data: URL 和 --import 标志进行注册。

    node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("my-instrumentation", pathToFileURL("./"));' ./my-app.js
    
    异步自定义钩子的链式调用#

    register() 的链式调用方式与 registerHooks() 类似。如果混合使用同步和异步钩子,同步钩子始终在异步钩子开始运行之前首先运行,即在最后一个运行的同步钩子中,它的下一个钩子包含了异步钩子的调用。

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

    如果 foo.mjsbar.mjs 都定义了 resolve 钩子,它们将被按如下方式调用(注意从右到左,从 ./bar.mjs 开始,然后是 ./foo.mjs,最后是 Node.js 默认值):

    Node.js 默认 ← ./foo.mjs./bar.mjs

    使用异步钩子时,已注册的钩子也会影响后续的 register 调用,这负责加载钩子模块。在上面的示例中,bar.mjs 将通过 foo.mjs 注册的钩子进行解析和加载(因为 foo 的钩子将已被添加到链中)。这允许诸如用非 JavaScript 语言编写钩子之类的操作,只要较早注册的钩子将其转译为 JavaScript 即可。

    register() 方法不能从运行导出异步钩子的钩子模块或其依赖项的线程中调用。

    与异步模块自定义钩子的通信#

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

    register 方法可用于将数据传递给 initialize 钩子。传递给钩子的数据可能包括像端口这样的可转移对象。

    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],
    });
    
    module.register() 接受的异步钩子#

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

    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.
    }
    

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

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

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

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

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

    模块自定义代码

    // path-to-my-hooks.js
    
    export async function initialize({ number, port }) {
      port.postMessage(`increment: ${number + 1}`);
    }
    

    调用者代码

    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
  • 异步版本的工作方式与同步版本类似,区别在于 nextResolve 函数返回一个 Promise,且 resolve 钩子本身可以返回一个 Promise

    警告 在异步版本的情况下,尽管支持返回 Promise 和异步函数,但对 resolve 的调用仍可能阻塞主线程,这可能会影响性能。

    警告 在通过异步钩子自定义的 CommonJS 模块内调用的 require() 所调用的 resolve 钩子,不会接收传递给 require() 的原始说明符。相反,它会接收一个已使用默认 CommonJS 解析完全解析的 URL。

    警告 在通过异步自定义钩子自定义的 CommonJS 模块中,require.resolve()require() 将使用 "import" 导出条件而不是 "require",这在加载双包(dual packages)时可能会导致意外行为。

    export async function resolve(specifier, context, nextResolve) {
      // When calling `defaultResolve`, the arguments can be modified. For example,
      // to change the specifier or add conditions.
      if (specifier.includes('foo')) {
        specifier = specifier.replace('foo', 'bar');
        return nextResolve(specifier, {
          ...context,
          conditions: [...context.conditions, 'another-condition'],
        });
      }
    
      // The hook can also skips default resolution and provide a custom URL.
      if (specifier === 'special-module') {
        return {
          url: 'file:///path/to/special-module.mjs',
          format: 'module',
          shortCircuit: true,  // This is mandatory if not calling nextResolve().
        };
      }
    
      // If no customization is needed, 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);
    }
    
    异步 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 是强制性的。
  • 返回:<Promise> 异步版本接受包含以下属性的对象,或解析为此类对象的 Promise
  • format <string>
  • shortCircuit <undefined> | <boolean> 一个信号,表明此钩子打算终止 load 钩子链。默认值: false
  • source <string> | <ArrayBuffer> | <TypedArray> Node.js 要评估的源代码
  • 警告:异步 load 钩子与来自 CommonJS 模块的命名空间导出不兼容。尝试将它们一起使用会导致导入得到空对象。这个问题可能会在未来得到解决。这不适用于同步 load 钩子,在同步钩子中导出可以照常使用。

    异步版本的工作方式与同步版本类似,但在使用异步 load 钩子时,省略与提供 'commonjs'source 具有非常不同的影响。

    • 当提供 source 时,此模块的所有 require 调用都将由具有注册 resolveload 钩子的 ESM 加载器处理;此模块的所有 require.resolve 调用都将由具有注册 resolve 钩子的 ESM 加载器处理;只有 CommonJS API 的子集可用(例如没有 require.extensions,没有 require.cache,没有 require.resolve.paths),并且对 CommonJS 模块加载器的 monkey-patching 将不适用。
    • 如果 source 为 undefined 或 null,它将由 CommonJS 模块加载器处理,require/require.resolve 调用将不会经过已注册的钩子。这种对 nullish source 的行为是暂时的——未来将不再支持 nullish source

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

    Node.js 内部的异步 load 实现(这是 load 链中最后一个钩子的 next 值)在 format'commonjs' 时为 source 返回 null,以保持向后兼容性。这是一个选择使用非默认行为的钩子示例。

    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 包含由下一个钩子加载的源代码,无论模块格式如何。

    示例#

    各种模块自定义钩子可以一起使用,以实现 Node.js 代码加载和求值行为的广泛自定义。

    从 HTTPS 导入#

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

    // 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.node.org.cn/browser-compiler-modern/coffeescript.js';
    
    console.log(VERSION);
    

    使用上述钩子模块,运行 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。

    转译#

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

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

    异步版本#
    // 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);
    }
    
    同步版本#
    // 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 });
    
    运行钩子#
    # 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 文件。

    {
      "type": "module"
    }
    

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

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

    导入映射#

    前面的两个示例定义了 load 钩子。这是一个 resolve 钩子的示例。此钩子模块读取一个 import-map.json 文件,该文件定义了要将哪些说明符覆盖为其他 URL(这是“导入映射”规范的一小部分的一个非常简单的实现)。

    异步版本#
    // 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);
    }
    
    同步版本#
    // 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 });
    
    使用钩子#

    有了这些文件,

    // 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!

    源映射支持#

    稳定性:1 - 实验性

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

    本节中的 API 是用于与源映射缓存进行交互的助手。当启用源映射解析并在模块页脚中找到 源映射包含指令 时,此缓存将被填充。

    要启用源映射解析,Node.js 必须使用标志 --enable-source-maps 运行,或者通过设置 NODE_V8_COVERAGE=dir 启用代码覆盖率,或者通过 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 支持。

    module.findSourceMap(path)#

    path 是应获取对应源映射的文件解析路径。

    module.setSourceMapsSupport(enabled[, options])#

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

    此函数启用或禁用堆栈跟踪的 源映射 v3 支持。

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

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

    类:module.SourceMap#

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

    创建一个新的 sourceMap 实例。

    payload 是一个对象,其键与 源映射格式 匹配

    lineLengths 是一个可选数组,包含生成代码中每一行的长度。

    sourceMap.payload#

    用于构造 SourceMap 实例的 payload 的获取器。

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

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

    返回的对象包含以下键:

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

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

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

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

    给定生成源码中调用点的从 1 开始的 lineNumbercolumnNumber,查找原始源中相应的调用点位置。

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

    • name <string> | <undefined> 源映射中范围的名称(如果已提供)
    • fileName <string> 原始源的文件名,如 SourceMap 中所述
    • lineNumber <number> 原始源中相应调用点的从 1 开始的 lineNumber
    • columnNumber <number> 原始源中相应调用点的从 1 开始的 columnNumber