From efc7db4d468d1f44c2959b821082c9969f9d8ca3 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 16 Jun 2026 10:39:16 +0200 Subject: [PATCH] vfs: integrate with CJS and ESM module loaders Co-authored-by: James M Snell Signed-off-by: Matteo Collina --- doc/api/vfs.md | 193 +++++ lib/internal/modules/cjs/loader.js | 38 +- lib/internal/modules/esm/get_format.js | 4 +- lib/internal/modules/esm/load.js | 12 +- lib/internal/modules/esm/resolve.js | 44 +- lib/internal/modules/helpers.js | 213 ++++- lib/internal/modules/package_json_reader.js | 51 +- lib/internal/vfs/file_system.js | 18 + lib/internal/vfs/setup.js | 578 ++++++++++++-- test/parallel/test-vfs-import.mjs | 142 ++++ .../parallel/test-vfs-invalid-package-json.js | 45 ++ test/parallel/test-vfs-layer-id.js | 43 ++ .../parallel/test-vfs-module-hooks-cleanup.js | 115 +++ test/parallel/test-vfs-module-hooks.mjs | 725 ++++++++++++++++++ test/parallel/test-vfs-package-json-cache.js | 23 + test/parallel/test-vfs-package-json.js | 184 +++++ test/parallel/test-vfs-require.js | 405 ++++++++++ test/parallel/test-vfs-scoped-cache-purge.js | 55 ++ 18 files changed, 2802 insertions(+), 86 deletions(-) create mode 100644 test/parallel/test-vfs-import.mjs create mode 100644 test/parallel/test-vfs-invalid-package-json.js create mode 100644 test/parallel/test-vfs-layer-id.js create mode 100644 test/parallel/test-vfs-module-hooks-cleanup.js create mode 100644 test/parallel/test-vfs-module-hooks.mjs create mode 100644 test/parallel/test-vfs-package-json-cache.js create mode 100644 test/parallel/test-vfs-package-json.js create mode 100644 test/parallel/test-vfs-require.js create mode 100644 test/parallel/test-vfs-scoped-cache-purge.js diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 87b37819c0fe48..7da2ed886f2150 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -46,6 +46,11 @@ callback-based, and promise-based file system methods that mirror the shape of the [`node:fs`][] API. All paths are POSIX-style and absolute (starting with `/`). +By default, the file tree is private to the VFS instance. To expose +it through the global `node:fs` module, `require()`, and `import`, +call [`vfs.mount(prefix)`][]; call [`vfs.unmount()`][] (or rely on a +`using` declaration) to detach again. + ## `vfs.create([provider][, options])` + +* `prefix` {string} The path prefix where the VFS will be mounted. +* Returns: {VirtualFileSystem} The VFS instance, for chaining or `using`. + +Mounts the virtual file system at the specified path prefix. After +mounting, files in the VFS can be accessed through the `node:fs` +module — and resolved through `require()` and `import` — using paths +that start with the prefix. + +If a real file-system path already exists at the mount prefix, the +VFS **shadows** that path: every operation against a path under the +mount point is directed to the VFS until the VFS is unmounted. + +```cjs +const vfs = require('node:vfs'); +const fs = require('node:fs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/data.txt', 'Hello'); +myVfs.mount('/virtual'); + +fs.readFileSync('/virtual/data.txt', 'utf8'); // 'Hello' +``` + +Each `VirtualFileSystem` instance may be mounted at most once at a +time. Attempting to mount an already-mounted instance throws +`ERR_INVALID_STATE`. Mounting two instances at overlapping prefixes +(e.g., `/virtual` and `/virtual/sub`) also throws `ERR_INVALID_STATE`. + +The VFS supports the [Explicit Resource Management][] proposal. Use +a `using` declaration to unmount automatically when leaving scope: + +```cjs +const vfs = require('node:vfs'); +const fs = require('node:fs'); + +{ + using myVfs = vfs.create(); + myVfs.writeFileSync('/data.txt', 'Hello'); + myVfs.mount('/virtual'); + + fs.readFileSync('/virtual/data.txt', 'utf8'); // 'Hello' +} // VFS is automatically unmounted here + +fs.existsSync('/virtual/data.txt'); // false +``` + +### `vfs.unmount()` + + + +Unmounts the virtual file system. After unmounting, virtual files +are no longer reachable through `node:fs`, `require()`, or `import`. +The same instance may be mounted again, at the same or a different +prefix, by calling `mount()`. + +This method is idempotent: calling `unmount()` on a VFS that is not +currently mounted has no effect. + +### `vfs.mounted` + + + +* {boolean} + +`true` while the VFS is mounted; `false` otherwise. + +### `vfs.mountPoint` + + + +* {string | null} + +The current mount-point path as an absolute string, or `null` when +the VFS is not mounted. + +### `vfs.layerId` + + + +* {number} + +A per-process monotonically increasing identifier assigned at +construction. The id is stable across `mount()` / `unmount()` cycles +for the lifetime of the instance, and is independent of the order in +which VFS layers are mounted. + +The layer id is the building block for cache scoping (see +[Module loader integration][]): + +* it surfaces in `import.meta.url` for ES modules loaded from this + VFS, as a `?vfs-layer=` search parameter, so that the cascaded + loader's caches can be scoped per VFS; +* it appears in the `NODE_DEBUG=vfs` output for `register` and + `deregister` events; +* it appears in the `ERR_INVALID_STATE` error message thrown when two + VFS instances try to mount at overlapping prefixes. + +```cjs +const vfs = require('node:vfs'); + +const a = vfs.create(); +const b = vfs.create(); +console.log(a.layerId); // e.g. 0 +console.log(b.layerId); // a.layerId + 1 +``` + ### `vfs.provider`