Stability: 1 - Experimental
The node:vfs module provides a virtual file system that can be mounted
alongside the real file system. Virtual files can be read using standard node:fs
operations and loaded as modules using require() or import.
To access it:
import vfs from 'node:vfs';const vfs = require('node:vfs');This module is only available under the node: scheme.
The Virtual File System (VFS) allows you to create in-memory file systems that
integrate seamlessly with the Node.js node:fs module and module loading system. This
is useful for:
- Bundling assets in Single Executable Applications (SEA)
- Testing file system operations without touching the disk
- Creating virtual module systems
- Embedding configuration or data files in applications
The VFS supports two operating modes:
When mounted at a path prefix (e.g., /virtual), the VFS handles all
operations for paths starting with that prefix. The VFS completely shadows
any real file system paths under the mount point.
When created with { overlay: true }, the VFS selectively intercepts only
paths that exist within the VFS. Paths that don't exist in the VFS fall through
to the real file system. This is useful for mocking specific files while leaving
others unchanged.
const vfs = require('node:vfs');
const fs = require('node:fs');
// Overlay mode: only intercept files that exist in VFS
const myVfs = vfs.create({ overlay: true });
myVfs.writeFileSync('/etc/config.json', JSON.stringify({ mocked: true }));
myVfs.mount('/');
// This reads from VFS (file exists in VFS)
fs.readFileSync('/etc/config.json', 'utf8'); // '{"mocked": true}'
// This reads from real FS (file doesn't exist in VFS)
fs.readFileSync('/etc/hostname', 'utf8'); // Real file contentSee Security considerations for important warnings about overlay mode.
The following example shows how to create a virtual file system, add files,
and access them through the standard node:fs API:
import vfs from 'node:vfs';
import fs from 'node:fs';
// Create a new virtual file system
const myVfs = vfs.create();
// Create directories and files
myVfs.mkdirSync('/app');
myVfs.writeFileSync('/app/config.json', JSON.stringify({ port: 3000 }));
myVfs.writeFileSync('/app/greet.js', 'module.exports = (name) => "Hello, " + name + "!";');
// Mount the VFS at a path prefix
myVfs.mount('/virtual');
// Now standard fs operations work on the virtual files
const config = JSON.parse(fs.readFileSync('/virtual/app/config.json', 'utf8'));
console.log(config.port); // 3000
// Modules can be required from the VFS
const greet = await import('/virtual/app/greet.js');
console.log(greet.default('World')); // Hello, World!
// Clean up
myVfs.unmount();const vfs = require('node:vfs');
const fs = require('node:fs');
// Create a new virtual file system
const myVfs = vfs.create();
// Create directories and files
myVfs.mkdirSync('/app');
myVfs.writeFileSync('/app/config.json', JSON.stringify({ port: 3000 }));
myVfs.writeFileSync('/app/greet.js', 'module.exports = (name) => "Hello, " + name + "!";');
// Mount the VFS at a path prefix
myVfs.mount('/virtual');
// Now standard fs operations work on the virtual files
const config = JSON.parse(fs.readFileSync('/virtual/app/config.json', 'utf8'));
console.log(config.port); // 3000
// Modules can be required from the VFS
const greet = require('/virtual/app/greet.js');
console.log(greet('World')); // Hello, World!
// Clean up
myVfs.unmount();The VFS has the following limitations:
Native addons (.node files) cannot be loaded from the VFS. Native addons
must exist on the real file system because they are loaded by the operating
system's dynamic linker, which cannot access virtual files.
Child processes spawned via child_process.spawn(), child_process.exec(),
or similar methods cannot directly access VFS files. The child process runs
in a separate address space and does not inherit the parent's VFS mounts.
To share data with child processes, write files to the real file system or
use inter-process communication.
Each worker thread has its own independent VFS state. A VFS mounted in the main thread is not automatically available in worker threads. To use VFS in workers, create and mount a new VFS instance within each worker.
The fs.watch() and fs.watchFile() functions work with VFS files but use
polling internally rather than native file system notifications, since VFS
files exist only in memory.
When using VFS with Single Executable Applications, the useCodeCache option
in the SEA configuration does not currently apply to modules loaded from the
VFS. This is a current limitation due to incomplete implementation, not a
technical impossibility. Consider bundling the application to enable code
caching and do not rely on module loading in VFS.
provider{VirtualProvider} Optional provider instance. Defaults to a newMemoryProvider.options{Object}moduleHooks{boolean} Whether to enablerequire()/importhooks for loading modules from the VFS. Default:true.virtualCwd{boolean} Whether to enable virtual working directory support. Default:false.overlay{boolean} Whether to enable overlay mode. In overlay mode, the VFS only intercepts paths that exist in the VFS, allowing other paths to fall through to the real file system. Useful for mocking specific files while leaving others unchanged. See Security considerations for important warnings. Default:false.
- Returns: {VirtualFileSystem}
Creates a new VirtualFileSystem instance. If no provider is specified, a
MemoryProvider is used, which stores files in memory.
import vfs from 'node:vfs';
// Create with default MemoryProvider
const memoryVfs = vfs.create();
// Create with explicit provider
const customVfs = vfs.create(new vfs.MemoryProvider());
// Create with options only
const vfsWithOptions = vfs.create({ moduleHooks: false });const vfs = require('node:vfs');
// Create with default MemoryProvider
const memoryVfs = vfs.create();
// Create with explicit provider
const customVfs = vfs.create(new vfs.MemoryProvider());
// Create with options only
const vfsWithOptions = vfs.create({ moduleHooks: false });The VirtualFileSystem class provides a file system interface backed by a
provider. It supports standard file system operations and can be mounted to
make virtual files accessible through the node:fs module.
provider{VirtualProvider} The provider to use. Default:MemoryProvider.options{Object}moduleHooks{boolean} Enable module loading hooks. Default:true.virtualCwd{boolean} Enable virtual working directory. Default:false.
Creates a new VirtualFileSystem instance.
Multiple VirtualFileSystem instances can be created and used independently.
Each instance maintains its own file tree and can be mounted at different
paths. However, only one VFS can be mounted at a given path prefix at a time.
If two VFS instances are mounted at overlapping paths (e.g., /virtual and
/virtual/sub), the more specific path takes precedence for matching paths.
path{string} The new working directory path within the VFS.
Changes the virtual working directory. This only affects path resolution within
the VFS when virtualCwd is enabled in the constructor options.
Throws ERR_INVALID_STATE if virtualCwd was not enabled during construction.
When mounted with virtualCwd enabled, the VFS also hooks process.chdir() and
process.cwd() to support virtual paths transparently. In Worker threads,
process.chdir() to virtual paths will work, but attempting to change to real
file system paths will throw ERR_WORKER_UNSUPPORTED_OPERATION.
- Returns: {string|null}
Returns the current virtual working directory, or null if no virtual directory
has been set yet.
Throws ERR_INVALID_STATE if virtualCwd was not enabled during construction.
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 via the node:fs module using paths that start
with the prefix.
If a real file system path already exists at the mount prefix, the VFS shadows that path. All operations to paths under the mount prefix will be directed to the VFS, making the real files inaccessible until the VFS is unmounted. See Security considerations for important warnings about this behavior.
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/data.txt', 'Hello');
myVfs.mount('/virtual');
// Now accessible as /virtual/data.txt
require('node:fs').readFileSync('/virtual/data.txt', 'utf8'); // 'Hello'On Windows, mount paths use drive letters:
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/data.txt', 'Hello');
myVfs.mount('C:\\virtual');
// Now accessible as C:\virtual\data.txt
require('node:fs').readFileSync('C:\\virtual\\data.txt', 'utf8'); // 'Hello'The VFS supports the Explicit Resource Management proposal. Use the using
declaration to automatically unmount when leaving scope:
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 is unmounted- {boolean}
Returns true if the VFS is currently mounted.
- {string | null}
The current mount point as an absolute path, or null if not mounted.
- {boolean}
Returns true if overlay mode is enabled. In overlay mode, the VFS only
intercepts paths that exist in the VFS, allowing other paths to fall through
to the real file system.
- {VirtualProvider}
The underlying provider for this VFS instance. Can be used to access
provider-specific methods like setReadOnly() for MemoryProvider.
const vfs = require('node:vfs');
const myVfs = vfs.create();
// Access the provider
console.log(myVfs.provider.readonly); // false
myVfs.provider.setReadOnly();
console.log(myVfs.provider.readonly); // true- {boolean}
Returns true if the underlying provider is read-only.
Unmounts the virtual file system. After unmounting, virtual files are no longer
accessible through the node:fs module. The VFS can be remounted at the same or a
different path by calling mount() again. Unmounting also resets the virtual
working directory if one was set.
This method is idempotent: calling unmount() on an already unmounted VFS
has no effect.
The VirtualFileSystem class provides methods that mirror the node:fs module API.
All paths are relative to the VFS root (not the mount point).
These methods accept the same argument types as their node:fs counterparts,
including string, Buffer, TypedArray, and DataView where applicable.
When overlay mode is enabled, the following behavior applies to node:fs operations
on mounted paths.
Path encoding: The VFS uses UTF-8 encoding for file and directory names internally. In overlay mode, path matching is performed using the VFS's UTF-8 encoding. When falling through to the real file system, paths are passed to the native file system APIs which handle encoding according to platform conventions (UTF-8 on most Unix systems, UTF-16 on Windows). This means the VFS inherits the underlying file system's encoding behavior for paths that fall through, while VFS-internal paths always use UTF-8.
Case sensitivity: The VFS is always case-sensitive internally. In overlay mode, this can cause unexpected behavior when overlaying a case-insensitive file system (such as macOS HFS+ or Windows NTFS):
- A VFS file at
/Data.txtwill not shadow a real file at/data.txt - Looking up
/DATA.TXTwill fall through to the real file system (not found in case-sensitive VFS), potentially finding a real file with different casing - This mismatch is intentional: the VFS maintains consistent cross-platform behavior rather than emulating the underlying file system's case handling
If case-insensitive matching is required, applications should normalize paths before VFS operations.
Operation routing:
- Read operations (
readFile,readdir,stat,lstat,access,exists,realpath,readlink): Check VFS first. If the path doesn't exist in VFS, fall through to the real file system. - Write operations (
writeFile,appendFile,mkdir,rename,unlink,rmdir,symlink,copyFile): Always operate on VFS. New files are created in VFS, and attempting to modify a real file that doesn't exist in VFS will create a new VFS file instead. - File descriptors: Once a file is opened, all subsequent operations on that descriptor stay within the same layer (VFS or real FS) where it was opened.
The VirtualFileSystem class supports all common synchronous node:fs methods
for reading, writing, and managing files and directories. Methods mirror the
node:fs module API.
The following node:fs sync methods have no VFS equivalent:
chmodSync()/fchmodSync()- VFS does not support permission changeschownSync()/fchownSync()- VFS does not support ownership changestruncateSync()/ftruncateSync()- UsewriteFileSync()insteadutimesSync()/futimesSync()/lutimesSync()- VFS does not support changing timestampslinkSync()- VFS does not support hard links (usesymlinkSync())fdatasyncSync()/fsyncSync()- Not applicable to in-memory storage
All synchronous methods have promise-based equivalents available through
vfs.promises:
import vfs from 'node:vfs';
const myVfs = vfs.create();
await myVfs.promises.writeFile('/data.txt', 'Hello');
const content = await myVfs.promises.readFile('/data.txt', 'utf8');
console.log(content); // 'Hello'const vfs = require('node:vfs');
const myVfs = vfs.create();
async function example() {
await myVfs.promises.writeFile('/data.txt', 'Hello');
const content = await myVfs.promises.readFile('/data.txt', 'utf8');
console.log(content); // 'Hello'
}The VirtualProvider class is an abstract base class for VFS providers.
Providers implement the actual file system storage and operations.
- {boolean}
Returns true if the provider is read-only.
- {boolean}
Returns true if the provider supports symbolic links.
To create a custom provider, extend VirtualProvider and implement the
required methods:
const { VirtualProvider } = require('node:vfs');
class MyProvider extends VirtualProvider {
get readonly() { return false; }
get supportsSymlinks() { return true; }
openSync(path, flags, mode) {
// Implementation
}
statSync(path, options) {
// Implementation
}
readdirSync(path, options) {
// Implementation
}
// ... implement other required methods
}The MemoryProvider stores files in memory. It supports full read/write
operations and symbolic links.
const { create, MemoryProvider } = require('node:vfs');
const myVfs = create(new MemoryProvider());Sets the provider to read-only mode. Once set to read-only, the provider cannot be changed back to writable. This is useful for finalizing a VFS after initial population.
const vfs = require('node:vfs');
const myVfs = vfs.create();
// Populate the VFS
myVfs.mkdirSync('/app');
myVfs.writeFileSync('/app/config.json', '{"readonly": true}');
// Make it read-only
myVfs.provider.setReadOnly();
// This would now throw an error
// myVfs.writeFileSync('/app/config.json', 'new content');The RealFSProvider wraps a real file system directory, allowing it to be
mounted at a different VFS path. This is useful for:
- Mounting a directory at a different path
- Enabling
virtualCwdsupport in Worker threads (by mounting the real file system through VFS) - Creating sandboxed views of real directories
rootPath{string} The real file system path to use as the provider root.
Creates a new RealFSProvider that wraps the specified directory. All paths
accessed through this provider are resolved relative to rootPath. Path
traversal outside rootPath (via ..) is prevented for security.
import vfs from 'node:vfs';
// Mount /home/user/project at /project
const projectVfs = vfs.create(new vfs.RealFSProvider('/home/user/project'));
projectVfs.mount('/project');
// Now /project/src/index.js maps to /home/user/project/src/index.js
import fs from 'node:fs';
const content = fs.readFileSync('/project/src/index.js', 'utf8');const vfs = require('node:vfs');
// Mount /home/user/project at /project
const projectVfs = vfs.create(new vfs.RealFSProvider('/home/user/project'));
projectVfs.mount('/project');
// Now /project/src/index.js maps to /home/user/project/src/index.js
const fs = require('node:fs');
const content = fs.readFileSync('/project/src/index.js', 'utf8');- {string}
The real file system path that this provider wraps.
When a VFS is mounted, the standard node:fs module automatically routes operations
to the VFS for paths that match the mount prefix:
import vfs from 'node:vfs';
import fs from 'node:fs';
const myVfs = vfs.create();
myVfs.writeFileSync('/hello.txt', 'Hello from VFS!');
myVfs.mount('/virtual');
// These all work transparently
fs.readFileSync('/virtual/hello.txt', 'utf8'); // Sync
await fs.promises.readFile('/virtual/hello.txt', 'utf8'); // Promise
fs.createReadStream('/virtual/hello.txt'); // Stream
// Real file system is still accessible
fs.readFileSync('/etc/passwd'); // Real fileconst vfs = require('node:vfs');
const fs = require('node:fs');
const myVfs = vfs.create();
myVfs.writeFileSync('/hello.txt', 'Hello from VFS!');
myVfs.mount('/virtual');
// These all work transparently
fs.readFileSync('/virtual/hello.txt', 'utf8'); // Sync
fs.promises.readFile('/virtual/hello.txt', 'utf8'); // Promise
fs.createReadStream('/virtual/hello.txt'); // Stream
// Real file system is still accessible
fs.readFileSync('/etc/passwd'); // Real fileVirtual files can be loaded as modules using require() or import:
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.writeFileSync('/math.js', `
exports.add = (a, b) => a + b;
exports.multiply = (a, b) => a * b;
`);
myVfs.mount('/modules');
const math = require('/modules/math.js');
console.log(math.add(2, 3)); // 5import vfs from 'node:vfs';
const myVfs = vfs.create();
myVfs.writeFileSync('/greet.mjs', `
export default function greet(name) {
return \`Hello, \${name}!\`;
}
`);
myVfs.mount('/modules');
const { default: greet } = await import('/modules/greet.mjs');
console.log(greet('World')); // Hello, World!The VFS returns real {fs.Stats} objects from stat(), lstat(), and fstat()
operations. These Stats objects behave identically to those returned by the real
file system:
stats.isFile(),stats.isDirectory(),stats.isSymbolicLink()work correctlystats.sizereflects the actual content sizestats.mtime,stats.ctime,stats.birthtimeare tracked per filestats.modeincludes the file type bits and permissions
When running as a Single Executable Application (SEA) with "useVfs": true in
the SEA configuration, bundled assets are automatically mounted at /sea. No
additional setup is required:
// In your SEA entry script
const fs = require('node:fs');
// Access bundled assets directly - they are automatically available at /sea
const config = JSON.parse(fs.readFileSync('/sea/config.json', 'utf8'));
const template = fs.readFileSync('/sea/templates/index.html', 'utf8');See the Single Executable Applications documentation for more information on creating SEA builds with assets.
The VFS supports symbolic links within the virtual file system. Symlinks are
created using vfs.symlinkSync() or vfs.promises.symlink() and can point
to files or directories within the same VFS.
Symbolic links in the VFS are VFS-internal only. They cannot:
- Point from a VFS path to a real file system path
- Point from a real file system path to a VFS path
- Be followed across VFS mount boundaries
When resolving symlinks, the VFS only follows links that target paths within the same VFS instance. Attempts to create symlinks with absolute paths that would resolve outside the VFS are allowed but will result in dangling symlinks.
const vfs = require('node:vfs');
const myVfs = vfs.create();
myVfs.mkdirSync('/data');
myVfs.writeFileSync('/data/config.json', JSON.stringify({}));
// This works - symlink within VFS
myVfs.symlinkSync('/data/config.json', '/config');
myVfs.readFileSync('/config', 'utf8'); // '{}'
// This creates a dangling symlink - target doesn't exist in VFS
myVfs.symlinkSync('/etc/passwd', '/passwd-link');
// myVfs.readFileSync('/passwd-link'); // Throws ENOENTIn overlay mode ({ overlay: true }), VFS and real file system symlinks remain
completely independent:
- VFS symlinks can only target other VFS paths. A VFS symlink cannot point to a real file system file, even if that file exists at the same logical path.
- Real file system symlinks can only target other real file system paths. A real symlink cannot point to a VFS file.
- No cross-layer resolution occurs. When following a symlink, the resolution stays entirely within either the VFS layer or the real file system layer.
const vfs = require('node:vfs');
const fs = require('node:fs');
const myVfs = vfs.create({ overlay: true });
myVfs.mkdirSync('/data');
myVfs.writeFileSync('/data/config.json', JSON.stringify({ source: 'vfs' }));
myVfs.symlinkSync('/data/config.json', '/data/link');
myVfs.mount('/app');
// VFS symlink resolves within VFS
fs.readFileSync('/app/data/link', 'utf8'); // '{"source": "vfs"}'
// If /app/data/real-link is a real FS symlink pointing to /app/data/config.json,
// it will NOT resolve to the VFS file - it looks for a real file at that pathThis design ensures predictable behavior: symlinks always resolve within their own layer, preventing unexpected interactions between virtual and real files.
VFS instances are not shared across worker threads. Each worker thread has its own V8 isolate and module cache, which means:
- A VFS mounted in the main thread is not accessible from worker threads
- Each worker thread must create and mount its own VFS instance
- VFS data is not synchronized between threads - changes in one thread are not visible in another
If you need to share virtual file content with worker threads, you must either:
- Recreate the VFS in each worker - Pass the data to workers via
workerDataand have each worker create its own VFS:
const { Worker, isMainThread, workerData } = require('node:worker_threads');
const vfs = require('node:vfs');
if (isMainThread) {
const fileData = { '/config.json': '{"key": "value"}' };
new Worker(__filename, { workerData: fileData });
} else {
// Worker: recreate VFS from passed data
const myVfs = vfs.create();
for (const [path, content] of Object.entries(workerData)) {
myVfs.writeFileSync(path, content);
}
myVfs.mount('/virtual');
// Now the worker has its own copy of the VFS
}- Use
RealFSProvider- If the data exists on the real file system, useRealFSProviderin each worker to mount the same directory.
Since process.chdir() is not available in Worker threads, you can use
RealFSProvider to enable virtual working directory support:
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
const vfs = require('node:vfs');
if (isMainThread) {
new Worker(__filename);
} else {
// In worker: mount real file system with virtualCwd enabled
const realVfs = vfs.create(
new vfs.RealFSProvider('/home/user/project'),
{ virtualCwd: true },
);
realVfs.mount('/project');
// Now we can use virtual chdir in the worker
realVfs.chdir('/project/src');
console.log(realVfs.cwd()); // '/project/src'
}This limitation exists because implementing cross-thread VFS access would require moving the implementation to C++ with shared memory management, which significantly increases complexity. This may be addressed in future versions.
When a VFS is mounted, it shadows any real file system paths under the mount prefix. This means:
- Real files at the mount path become inaccessible
- All operations are redirected to the VFS
- Modules loaded from shadowed paths will use VFS content
This behavior can be exploited maliciously. A module could mount a VFS over
critical system paths (like /etc on Unix or C:\Windows on Windows) and
intercept sensitive operations:
// WARNING: Example of dangerous behavior - DO NOT DO THIS
const vfs = require('node:vfs');
const maliciousVfs = vfs.create();
maliciousVfs.writeFileSync('/passwd', 'malicious content');
maliciousVfs.mount('/etc'); // Shadows /etc/passwd!
// Now fs.readFileSync('/etc/passwd') returns 'malicious content'Overlay mode ({ overlay: true }) allows a VFS to selectively intercept file
operations only for paths that exist in the VFS. While this is useful for
mocking specific files in tests, it can also be exploited to covertly intercept
access to specific files:
// WARNING: Example of dangerous behavior - DO NOT DO THIS
const vfs = require('node:vfs');
// Create an overlay VFS that intercepts a specific file
const spyVfs = vfs.create(new vfs.MemoryProvider(), { overlay: true });
spyVfs.writeFileSync('/etc/shadow', 'intercepted!');
spyVfs.mount('/'); // Mount at root with overlay mode
// Only /etc/shadow is intercepted, other files work normally
fs.readFileSync('/etc/passwd'); // Real file (works normally)
fs.readFileSync('/etc/shadow'); // Returns 'intercepted!' (mocked)This is particularly dangerous because:
- It is harder to detect than full path shadowing.
- Only specific targeted files are affected.
- Other operations appear to work normally.
To help detect unauthorized VFS usage, node:process emits events when
a VFS is mounted or unmounted:
process.on('vfs-mount', (info) => {
console.log(`VFS mounted at ${info.mountPoint}`);
console.log(` overlay: ${info.overlay}`);
console.log(` readonly: ${info.readonly}`);
});
process.on('vfs-unmount', (info) => {
console.log(`VFS unmounted from ${info.mountPoint}`);
});The event object contains:
mountPoint{string} The path where the VFS is mounted.overlay{boolean} Whether overlay mode is enabled.readonly{boolean} Whether the VFS is read-only.
Applications can use these events to log VFS activity, alert on suspicious mounts (such as mounting over system paths), or enforce security policies.
- Audit dependencies: Be cautious of third-party modules that use VFS, as they could shadow important paths.
- Use unique mount points: Mount VFS at paths that don't conflict with
real file system paths, such as
/@virtualor/vfs-{unique-id}. - Verify mount points: Before trusting file content from paths that could be shadowed, verify the mount state.
- Limit VFS usage: Only use VFS in controlled environments where you trust all loaded modules.