A first-class virtual file system module (node:vfs) with a provider-based architecture that integrates with Node.js's fs module and module loader.
Key Features
-
Provider Architecture - Extensible design with pluggable providers:
MemoryProvider- In-memory file system with full read/write supportSEAProvider- Read-only access to Single Executable Application assetsVirtualProvider- Base class for creating custom providers
-
Standard fs API - Uses familiar
writeFileSync,readFileSync,mkdirSyncinstead of custom methods -
Mount Mode - VFS mounts at a specific path prefix (e.g.,
/virtual), clear separation from real filesystem -
Module Loading -
require()andimportwork seamlessly from virtual files -
SEA Integration - Assets automatically mounted at
/seawhen running as a Single Executable Application -
Full fs Support - readFile, stat, readdir, exists, streams, promises, glob, symlinks
Example
const vfs = require('node:vfs'); const fs = require('node:fs'); // Create a VFS with default MemoryProvider const myVfs = vfs.create(); // Use standard fs-like API myVfs.mkdirSync('/app'); myVfs.writeFileSync('/app/config.json', '{"debug": true}'); myVfs.writeFileSync('/app/module.js', 'module.exports = "hello"'); // Mount to make accessible via fs module myVfs.mount('/virtual'); // Works with standard fs APIs const config = JSON.parse(fs.readFileSync('/virtual/app/config.json', 'utf8')); const mod = require('/virtual/app/module.js'); // Cleanup myVfs.unmount();
SEA Usage
When running as a Single Executable Application, bundled assets are automatically available:
const fs = require('node:fs'); // Assets are automatically mounted at /sea - no setup required const config = fs.readFileSync('/sea/config.json', 'utf8'); const template = fs.readFileSync('/sea/templates/index.html', 'utf8');
Public API
const vfs = require('node:vfs'); vfs.create([provider][, options]) // Create a VirtualFileSystem vfs.VirtualFileSystem // The main VFS class vfs.VirtualProvider // Base class for custom providers vfs.MemoryProvider // In-memory provider vfs.SEAProvider // SEA assets provider (read-only)
Disclaimer: I've used a significant amount of Claude Code tokens to create this PR. I've reviewed all changes myself.
F.A.Q.
Why is this PR massive?
This PR is massive because the goal is to intercept all fs and fs.promises methods, as well as the module-loading system. This involves 164+ interception points inside existing Node.js functions.
By total churn (additions + deletions) as of 2026/03/23:
- Tests: 11,202 — 51.6%
- Code: 9,234 — 42.5%
- Docs: 1,281 — 5.9%
Why was a significant portion of code generated by AI?
No one tackled this problem before because of its sheer size. AI made it possible.
Adding 164+ integrations points by hand is extremely laborious.
Why was this PR not split into multiple chunks?
The key important part is to validate that the integration design is correct. It's extremely hard to separate that from its actual usage and avoid significant rework/integration.
Should we put it behind a flag?
We could. The high-risk parts (the integration points) will still be exercised, even if they are behind a flag.
More questions will be added as they pop up
Review Guide
Bottom-up walkthrough of the Virtual File System implementation. If you only care about the interception points, you should read subsections 3, 4, and 6.
1. Data model
provider.js —
VirtualProvider is the abstract storage backend. Subclasses implement
open, stat, readdir, mkdir, rmdir, unlink, rename (sync + async pairs).
Derived operations (readFile, writeFile, copyFile, access, realpath, …)
are built on top. Three flags control optional features: readonly, supportsSymlinks, supportsWatch.
file_handle.js —
VirtualFileHandle is per-open-file state with read/write/stat/truncate/close
(sync + async). MemoryFileHandle extends it with a Buffer backend and geometric
doubling for writes.
providers/memory.js —
Default provider. Tree of MemoryEntry nodes (file, dir, symlink). Supports hard links,
symlinks with cycle detection, lazy populate callbacks, dynamic contentProvider functions,
and irreversible setReadOnly().
providers/real.js —
Wraps a real directory, re-mounted at a different prefix. Prevents traversal outside rootPath.
2. VirtualFileSystem
file_system.js —
User-facing class (via node:vfs).
Wraps a provider, adds mount/unmount lifecycle and path translation.
mount('/prefix') registers the VFS, triggers handler installation on first mount.
unmount() deregisters, clears handlers if last VFS, flushes CJS caches.
Exposes the full node:fs surface (sync, callback, promise) with automatic path translation.
3. Injection: setup.js
setup.js —
Central wiring. createVfsHandlers() returns a frozen object with a method for every
intercepted fs operation. Every method returns undefined to fall through to the real fs,
or a value/Promise for VFS-handled paths.
Registration flow: registerVFS() → push to activeVFSList → first mount calls
installHooks() → createVfsHandlers() + setVfsHandlers() + module loader overrides.
Deregistration reverses this and clears CJS path caches.
Design note: per-function hooks — VFS uses per-function handler objects
rather than a Proxy or dispatch table. This avoids adding overhead to every
fs call when no VFS is active (vfsState.handlers === null is a single
null-check). New fs APIs that should be VFS-aware must add a corresponding
hook in createVfsHandlers() (setup.js).
4. fs integration
lib/internal/fs/utils.js —
Holds vfsState = { handlers: null }. Every fs function checks handlers !== null.
lib/fs.js —
Callback/sync functions use vfsVoid(promise, cb) and vfsResult(promise, cb) to bridge
VFS promises into callbacks. Multi-value callbacks (read/write/readv/writev) use inline
PromisePrototypeThen. Sync functions check for undefined return from sync handlers.
lib/internal/fs/promises.js —
Same undefined-check pattern inside async functions.
Only glob()/globSync() are not intercepted.
5. Virtual file descriptors
fd.js —
VFS FDs start at 10,000 (no collision with OS FDs). openVirtualFd() allocates,
getVirtualFd() looks up, closeVirtualFd() deletes. Every FD-based fs function
calls getVirtualFd(fd) — returns VirtualFD or undefined (fall through).
6. Module loader
lib/internal/modules/helpers.js —
Wrapper functions (loaderStat, loaderReadFile, loaderRealpath, loaderReadPackageJSON, …)
that check a VFS override before falling through to native C++ bindings. null by default
(zero overhead); setup.js installs overrides via setLoaderFsOverrides() and
setLoaderPackageOverrides() on first mount. CJS and ESM loaders both go through these wrappers.
7. Streams and watchers
streams.js —
VirtualReadStream (Readable) and VirtualWriteStream (Writable), same events as real-fs streams.
watcher.js —
Polling-based (no OS notifications for in-memory files). VFSWatcher for fs.watch(),
VFSStatWatcher for fs.watchFile(), VFSWatchAsyncIterable for fs.promises.watch().
8. SEA integration
src/node_sea.cc —
"useVfs": true in SEA config sets kEnableVfs flag (bit 5 of SeaFlags).
Assets are serialized into the blob; main script auto-included. C++ bindings expose
isVfsEnabled(), getAsset(), getAssetKeys() via internalBinding('sea').
lib/internal/vfs/providers/sea.js —
Read-only provider backed by executable memory (zero-copy via getAsset()).
Automatically derives directory structure from asset key paths.
lib/internal/main/embedding.js —
Calls initSeaVfs() before running main script. Mounts at /sea, rewrites CJS entry
to /sea/<main> so require() and relative paths work through VFS hooks from the start.
9. Mocking with overlay mode
file_system.js —
vfs.create({ overlay: true }) enables overlay mode: the VFS only intercepts paths that
exist inside it, everything else falls through to the real filesystem. This turns VFS into
a surgical mocking layer — mount at a real directory, write the files you want to replace,
and leave the rest untouched.
const myVfs = vfs.create({ overlay: true }); myVfs.writeFileSync('/config.json', '{"env":"test"}'); myVfs.writeFileSync('/lib/db.js', 'module.exports = { query: () => [] }'); myVfs.mount('/app'); fs.readFileSync('/app/config.json'); // returns VFS content fs.readFileSync('/app/index.js'); // falls through to real fs require('/app/lib/db.js'); // loads the mocked module
The key mechanism is shouldHandle(): in overlay mode it calls statSync() on the provider
before claiming the path. Non-overlay mode claims all paths under the mount prefix.
This works across require(), import, workers (virtualCwd: true), and all node:fs APIs.
10. node:test mock.fs()
lib/internal/test_runner/mock/mock.js —
t.mock.fs() is the test-runner integration. It creates an overlay-mode VFS with
moduleHooks: true, mounts it, and returns a MockFSContext that auto-restores
when the test ends (via t.mock cleanup).
test('reads config from virtual file', (t) => { t.mock.fs({ prefix: '/app', files: { '/config.json': '{"env":"test"}', '/lib/utils.js': 'module.exports = { sum: (a, b) => a + b }', }, }); assert.strictEqual(fs.readFileSync('/app/config.json', 'utf8'), '{"env":"test"}'); assert.strictEqual(require('/app/lib/utils.js').sum(1, 2), 3); // auto-unmounts after test });
MockFSContext exposes addFile(), addDirectory(), existsSync(), and restore()
for dynamic manipulation. The underlying vfs property gives direct access to the
VirtualFileSystem instance. Multiple mock.fs() calls can coexist with different prefixes.
Fixes #60021