模块加载器集成


🌐 Module loader integration

一旦挂载了 VirtualFileSystem,挂载点下的路径就会参与模块解析和加载。require() 和 require.resolve() 使用的 CommonJS 解析算法,以及 import 和 import.meta.resolve() 使用的 ES 模块解析算法 都保持不变;相反,这些算法执行的每个文件系统操作都会在被探测的路径上进行派发:挂载点下的路径由拥有它的 VFS 提供服务,其他路径则由真实文件系统提供服务。因此,从 VFS 提供的文件表现得就像一流的模块。

🌐 Once a VirtualFileSystem is mounted, paths under the mount point participate in module resolution and loading. The CommonJS resolution algorithm used by require() and require.resolve() and the ES modules resolution algorithm used by import and import.meta.resolve() are unchanged; instead, every file system operation those algorithms perform is dispatched on the path being probed: paths under a mount point are served by the owning VFS, and all other paths are served by the real file system. Files served from the VFS therefore behave as first-class modules.

因为挂载路径存在于一个保留的命名空间中,这个命名空间在磁盘上不存在,所以任何给定的路径要么由一个 VFS 提供,要么由真实文件系统提供,但不会同时由两者提供。两者之间没有搜索顺序或回退机制:如果挂载点下的路径在 VFS 中不存在,解析将以 ENOENT 失败,不会去检查磁盘,并且挂载层永远不会覆盖真实目录。

🌐 Because mounted paths live in a reserved namespace that cannot exist on disk, any given path is served either by exactly one VFS or by the real file system, never both. There is no search order or fallback between the two: if a path under a mount point does not exist in the VFS, resolution fails with ENOENT without consulting the disk, and a mounted layer never shadows a real directory.

为了解析的目的,挂载点的行为类似于文件系统根目录:package.json。作用域查找和 正在从 node_modules 加载文件夹 会在挂载点停止。例如,当 ${mountPoint}/foo/bar/main.cjs 调用 require('baz') 时,查找过程如下:

🌐 For resolution purposes the mount point behaves as a file system root: package.json scope lookups and loading from node_modules folders stop at the mount point. For example, when ${mountPoint}/foo/bar/main.cjs calls require('baz'), the lookup goes through:

  • ${mountPoint}/foo/bar/node_modules/baz
  • ${mountPoint}/foo/node_modules/baz
  • ${mountPoint}/node_modules/baz
  • 如果设置了 $NODE_PATH,$NODE_PATH 中列出的文件夹
  • $HOME/.node_modules/baz
  • $HOME/.node_libraries/baz
  • $PREFIX/lib/node/baz

最后四个条目是 全局文件夹,它们属于传统的 CommonJS 行为,并不适用于 import。绝对标识符可以在任意方向跨越边界:真实文件系统上的模块可以 require() 一个挂载路径,而虚拟模块可以 require() 一个真实的模块。

🌐 The last four entries are the global folders, which are legacy CommonJS behavior and do not apply to import. Absolute specifiers may cross the boundary in either direction: a module on the real file system can require() a mounted path, and a virtual module can require() a real one.

const vfs = require('node:vfs');

const myVfs = vfs.create();
myVfs.mkdirSync('/lib');
myVfs.writeFileSync('/lib/greet.js', 'module.exports = () => "hi";');
myVfs.writeFileSync(
  '/lib/package.json', '{"main": "./greet.js"}');
const mountPoint = myVfs.mount();

const greet = require(`${mountPoint}/lib`);
console.log(greet()); // 'hi'

myVfs.unmount(); 

对于 ECMAScript 模块,在将挂载路径传递给动态 import() 时使用 file: URL。vfs.mountPointURL 以这种形式提供挂载点;这可以让 VFS 导入在 Windows 上保持可移植,因为挂载路径使用 Windows 路径语法。

🌐 For ECMAScript modules, use file: URLs when passing mounted paths to dynamic import(). vfs.mountPointURL provides the mount point in that form; this keeps VFS imports portable on Windows, where mounted paths use Windows path syntax.

import vfs from 'node:vfs';

const myVfs = vfs.create();
myVfs.writeFileSync('/mod.mjs', 'export const value = 42;');
myVfs.mount();

const { value } = await import(`${myVfs.mountPointURL}/mod.mjs`);
console.log(value); // 42

myVfs.unmount(); 

从挂载的虚拟文件系统(VFS)加载的 CommonJS 模块通过以挂载点开头的 VFS 路径来识别。这可以在模块中的 __filename 和 __dirname,或者涉及 VFS 模块函数的错误堆栈跟踪中看到。VFS 中的 ES 模块也通过其 VFS 路径的 file: URL 来识别,这同样可以在例如 import.meta.url 中看到。

🌐 CommonJS modules loaded from a mounted VFS are identified by their VFS paths that start with the mount point. This is reflected in, for example, __filename and __dirname in the module, or the errors stack traces involving functions from the VFS modules. ES modules in the VFS are similarly identified by the file: URL of their VFS paths and this is reflected in e.g. import.meta.url.

就像从真实文件系统加载的模块一样,从 VFS 加载的模块在第一次加载时也会被缓存。当使用 require() 或 import() 多次加载属于挂载 VFS 的绝对路径或 URL 时,模块只会被加载一次,之后的调用都会返回同一个实例。

🌐 Like modules loaded from the real file system, modules loaded from the VFS are cached on the first load. When require() or import() is used to load an absolute path or URL that falls under the mounted VFS multiple times, the module is only loaded once and subsequent calls return the same instance.

调用 vfs.unmount() 会使从挂载点加载的模块失效:在重新创建的挂载下对某个路径进行后续的 require() 或 import 操作,会从新挂载的虚拟文件系统重新读取文件,而不是返回旧的模块。从其他虚拟文件系统实例或真实文件系统加载的模块不受影响。

🌐 Calling vfs.unmount() invalidates the modules that were loaded from the mount point: a subsequent require() or import of a path under a re-created mount re-reads the file from the newly mounted VFS rather than returning a stale module. Modules loaded from other VFS instances or from the real file system are unaffected.

挂载和卸载不会停止已经开始的任何模块执行,也不会使已经从 VFS 模块中生成的任何对象失效。就像真实文件系统中的模块一样,调用者需要自己负责在虚拟文件系统中加载模块时避免删除或使模块失效。

🌐 Mounting and unmounting do not stop any module execution that is already started, or invalidate any objects materialized from VFS modules that are already executed. As with modules in the real file system, the callers are responsible for avoiding removal or invalidation of modules in the virtual file system while they are being loaded.

存储在挂载的虚拟文件系统(VFS)中的本地插件(.node 文件)也可以被 require()。操作系统的动态加载器无法打开虚拟路径,因此插件的字节会从 VFS 读取,并从一个私有的、自清理的临时镜像加载。实际文件系统上的插件不受影响,可以直接加载。

🌐 Native addons (.node files) stored in a mounted VFS can be require()d as well. The operating system's dynamic loader cannot open a virtual path, so the addon's bytes are read from the VFS and loaded from a private, self-cleaning temporary image instead. Addons on the real file system are unaffected and load directly.

通过 ffi.dlopen()(或 new ffi.DynamicLibrary())打开的共享库工作原理相同:在挂载的 VFS 中检测到库路径,从 VFS 中读取其字节,然后库从一个私有的、自清理的镜像中加载,同时 library.path 继续报告虚拟路径。真实文件系统上的库则直接加载。

🌐 Shared libraries opened through ffi.dlopen() (or new ffi.DynamicLibrary()) work the same way: a library path inside a mounted VFS is detected, its bytes are read from the VFS, and the library is loaded from a private, self-cleaning image while library.path keeps reporting the virtual path. Libraries on the real file system load directly.