Virtual File System for Node.js - #61478
Closed
mcollina wants to merge 133 commits into
Closed
Conversation
Collaborator
|
Review requested:
|
mcollina
requested review from
Ethan-Arrowood,
RaisinTen,
avivkeller and
joyeecheung
January 22, 2026 18:04
Contributor
|
The
notable-change
Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. Otherwise, the commit will be placed in the Other Notable Changes section. |
Contributor
|
Nice! This is a great addition. Since it's such a large PR, this will take me some time to review. Will try to tackle it over the next week. |
avivkeller
reviewed
Jan 22, 2026
| */ | ||
| existsSync(path) { | ||
| // Prepend prefix to path for VFS lookup | ||
| const fullPath = this.#prefix + (StringPrototypeStartsWith(path, '/') ? path : '/' + path); |
Member
There was a problem hiding this comment.
Can we use path.join?
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 providersStandard fs API - Uses familiar
writeFileSync,readFileSync,mkdirSyncinstead of custom methodsMount Mode - VFS mounts at a specific path prefix (e.g.,
/virtual), clear separation from real filesystemModule Loading -
require()andimportwork seamlessly from virtual filesSEA Integration - Assets automatically mounted at
/seawhen running as a Single Executable ApplicationFull fs Support - readFile, stat, readdir, exists, streams, promises, glob, symlinks
Example
SEA Usage
When running as a Single Executable Application, bundled assets are automatically available:
Public API
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
fsandfs.promisesmethods, 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:
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โVirtualProvideris the abstract storage backend. Subclasses implementopen,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โVirtualFileHandleis per-open-file state withread/write/stat/truncate/close(sync + async).
MemoryFileHandleextends it with aBufferbackend and geometricdoubling for writes.
providers/memory.jsโDefault provider. Tree of
MemoryEntrynodes (file, dir, symlink). Supports hard links,symlinks with cycle detection, lazy
populatecallbacks, dynamiccontentProviderfunctions,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:fssurface (sync, callback, promise) with automatic path translation.3. Injection: setup.js
setup.jsโCentral wiring.
createVfsHandlers()returns a frozen object with a method for everyintercepted fs operation. Every method returns
undefinedto fall through to the real fs,or a value/Promise for VFS-handled paths.
Registration flow:
registerVFS()โ push toactiveVFSListโ first mount callsinstallHooks()โ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
fscall when no VFS is active (vfsState.handlers === nullis a singlenull-check). New
fsAPIs that should be VFS-aware must add a correspondinghook in
createVfsHandlers()(setup.js).4. fs integration
lib/internal/fs/utils.jsโHolds
vfsState = { handlers: null }. Every fs function checkshandlers !== null.lib/fs.jsโCallback/sync functions use
vfsVoid(promise, cb)andvfsResult(promise, cb)to bridgeVFS promises into callbacks. Multi-value callbacks (read/write/readv/writev) use inline
PromisePrototypeThen. Sync functions check forundefinedreturn from sync handlers.lib/internal/fs/promises.jsโSame
undefined-check pattern insideasyncfunctions.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 functioncalls
getVirtualFd(fd)โ returnsVirtualFDorundefined(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.
nullby default(zero overhead);
setup.jsinstalls overrides viasetLoaderFsOverrides()andsetLoaderPackageOverrides()on first mount. CJS and ESM loaders both go through these wrappers.7. Streams and watchers
streams.jsโVirtualReadStream(Readable) andVirtualWriteStream(Writable), same events as real-fs streams.watcher.jsโPolling-based (no OS notifications for in-memory files).
VFSWatcherforfs.watch(),VFSStatWatcherforfs.watchFile(),VFSWatchAsyncIterableforfs.promises.watch().8. SEA integration
src/node_sea.ccโ"useVfs": truein SEA config setskEnableVfsflag (bit 5 ofSeaFlags).Assets are serialized into the blob; main script auto-included. C++ bindings expose
isVfsEnabled(),getAsset(),getAssetKeys()viainternalBinding('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 entryto
/sea/<main>sorequire()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 thatexist 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.
The key mechanism is
shouldHandle(): in overlay mode it callsstatSync()on the providerbefore claiming the path. Non-overlay mode claims all paths under the mount prefix.
This works across
require(),import, workers (virtualCwd: true), and allnode:fsAPIs.10.
node:testmock.fs()lib/internal/test_runner/mock/mock.jsโt.mock.fs()is the test-runner integration. It creates an overlay-mode VFS withmoduleHooks: true, mounts it, and returns aMockFSContextthat auto-restoreswhen the test ends (via
t.mockcleanup).MockFSContextexposesaddFile(),addDirectory(),existsSync(), andrestore()for dynamic manipulation. The underlying
vfsproperty gives direct access to theVirtualFileSysteminstance. Multiplemock.fs()calls can coexist with different prefixes.Fixes #60021