diff --git a/benchmark/vfs/module-graph.js b/benchmark/vfs/module-graph.js new file mode 100644 index 000000000000..aee5e8616198 --- /dev/null +++ b/benchmark/vfs/module-graph.js @@ -0,0 +1,62 @@ +'use strict'; +const path = require('path'); +const { pathToFileURL } = require('url'); +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + type: ['cjs', 'esm'], + files: [1e2, 1e3], + n: [10], +}, { flags: ['--experimental-vfs', '--no-warnings'] }); + +// Builds a module graph of `files` packages, each with its own package.json, +// an index requiring a package-local file and a shared root module, and an +// entry point that pulls in every package. +function buildGraph(layer, files, type) { + const entryRequires = []; + if (type === 'esm') { + layer.writeFileSync('/package.json', '{"type":"module"}'); + } + layer.writeFileSync('/shared.js', + type === 'cjs' ? 'module.exports = 0;' : 'export default 0;'); + for (let i = 0; i < files; i++) { + layer.mkdirSync(`/${i}`, { recursive: true }); + if (type === 'cjs') { + layer.writeFileSync(`/${i}/package.json`, '{"main":"index.js"}'); + layer.writeFileSync(`/${i}/lib.js`, 'module.exports = 1;'); + layer.writeFileSync( + `/${i}/index.js`, + 'require("./lib.js"); require("../shared.js"); module.exports = __filename;'); + entryRequires.push(`require('./${i}/');`); + } else { + layer.writeFileSync(`/${i}/package.json`, '{"type":"module"}'); + layer.writeFileSync(`/${i}/lib.js`, 'export default 1;'); + layer.writeFileSync( + `/${i}/index.js`, + 'import "./lib.js"; import "../shared.js"; export default import.meta.url;'); + entryRequires.push(`import './${i}/index.js';`); + } + } + layer.writeFileSync('/entry.js', entryRequires.join('\n')); +} + +async function main({ n, type, files }) { + const vfs = require('node:vfs'); + const layer = vfs.create(); + buildGraph(layer, files, type); + + bench.start(); + for (let i = 0; i < n; i++) { + const mountPoint = layer.mount(); + const entry = path.join(mountPoint, 'entry.js'); + if (type === 'cjs') { + require(entry); + } else { + await import(pathToFileURL(entry).href); + } + // Unmounting purges the module caches for the mount prefix, so every + // iteration is a cold load of the full graph. + layer.unmount(); + } + bench.end(n * files); +} diff --git a/doc/api/vfs.md b/doc/api/vfs.md index 1551a18feac7..2070efdac238 100644 --- a/doc/api/vfs.md +++ b/doc/api/vfs.md @@ -61,6 +61,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()`][]; call [`vfs.unmount()`][] (or rely on a +`using` declaration) to detach again. + ## `vfs.create([provider][, options])` + +* Returns: {string} The absolute mount point. + +Mounts the virtual file system and returns the resulting mount point. +After mounting, files in the VFS can be accessed through the +`node:fs` module and resolved through `require()` and `import` +using paths under the returned mount point. + +Mount points always live inside a reserved namespace that cannot have child file system entries, +so virtual paths never conflate with (or shadow) real paths. The virtual path scheme is subject to +change and users should not manually construct them based on assumptions. Instead, obtain +them from what `vfs.mount()` returns or `vfs.mountPoint`. + +```cjs +const vfs = require('node:vfs'); +const fs = require('node:fs'); + +const myVfs = vfs.create(); +myVfs.writeFileSync('/data.txt', 'Hello'); +const mountPoint = myVfs.mount(); +// e.g. '/dev/null/vfs/0' + +fs.readFileSync(`${mountPoint}/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`. Because each instance mounts inside its own +per-layer namespace, mounts from different instances can never +overlap. + +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'); + +let mountPoint; +{ + using myVfs = vfs.create(); + myVfs.writeFileSync('/data.txt', 'Hello'); + mountPoint = myVfs.mount(); + + fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello' +} // VFS is automatically unmounted here + +fs.existsSync(`${mountPoint}/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 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 as an absolute string (the value returned by +the last [`vfs.mount()`][] call), or `null` when the VFS is not +mounted. + +### `vfs.mountPointURL` + + + +* {string | null} + +The current mount point as a `file:` URL string (the [`vfs.mountPoint`][] +path converted with [`url.pathToFileURL()`][]), or `null` when the VFS +is not mounted. + +This is a convenience for addressing mounted files with URL-based +APIs such as dynamic `import()`: + +```mjs +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.provider`