| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
Review requested:
|
Sorry, something went wrong.
|
The notable-change PRs with changes that should be highlighted in changelogs. label has been added by @avivkeller. 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. |
Sorry, something went wrong.
|
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. |
Sorry, something went wrong.
| */ | ||
| existsSync(path) { | ||
| // Prepend prefix to path for VFS lookup | ||
| const fullPath = this.#prefix + (StringPrototypeStartsWith(path, '/') ? path : '/' + path); |
There was a problem hiding this comment.
Can we use path.join?
Sorry, something went wrong.
| validateObject(files, 'options.files'); | ||
| } | ||
|
|
||
| const { VirtualFileSystem } = require('internal/vfs/virtual_fs'); |
There was a problem hiding this comment.
Shouldn't we import this at the top level / lazy load it at the top level?
Sorry, something went wrong.
| ArrayPrototypePush(this.#mocks, { | ||
| __proto__: null, | ||
| ctx, | ||
| restore: restoreFS, |
There was a problem hiding this comment.
| restore: restoreFS, | |
| restore: ctx.restore, |
nit
Sorry, something went wrong.
| * @param {object} [options] Optional configuration | ||
| */ | ||
| addFile(name, content, options) { | ||
| const path = this._directory.path + '/' + name; |
There was a problem hiding this comment.
Can we use path.join?
Sorry, something went wrong.
| let entry = current.getEntry(segment); | ||
| if (!entry) { | ||
| // Auto-create parent directory | ||
| const dirPath = '/' + segments.slice(0, i + 1).join('/'); |
There was a problem hiding this comment.
Let's use path.join
Sorry, something went wrong.
| let entry = current.getEntry(segment); | ||
| if (!entry) { | ||
| // Auto-create parent directory | ||
| const parentPath = '/' + segments.slice(0, i + 1).join('/'); |
There was a problem hiding this comment.
path.join?
Sorry, something went wrong.
| } | ||
| } | ||
| callback(null, content); | ||
| }).catch((err) => { |
There was a problem hiding this comment.
| }).catch((err) => { | |
| }, (err) => { |
Sorry, something went wrong.
| const bytesToRead = Math.min(length, available); | ||
| content.copy(buffer, offset, readPos, readPos + bytesToRead); |
There was a problem hiding this comment.
Primordials?
Sorry, something went wrong.
| } | ||
|
|
||
| callback(null, bytesToRead, buffer); | ||
| }).catch((err) => { |
There was a problem hiding this comment.
| }).catch((err) => { | |
| }, (err) => { |
Sorry, something went wrong.
|
Left an initial review, but like @Ethan-Arrowood said, it'll take time for a more in depth look |
Sorry, something went wrong.
|
It's nice to see some momentum in this area, though from a first glance it seems the design has largely overlooked the feedback from real world use cases collected 4 years ago: https://github.com/nodejs/single-executable/blob/main/docs/virtual-file-system-requirements.md - I think it's worth checking that the API satisfies the constraints that users of this feature have provided, to not waste the work that have been done by prior contributors to gather them, or having to reinvent it later (possibly in a breaking manner) to satisfy these requirements from real world use cases. |
Sorry, something went wrong.
Codecov Report❌ Patch coverage is 92.31874% with 695 lines in your changes missing coverage. Please review. @@ Coverage Diff @@
## main #61478 +/- ##
==========================================
+ Coverage 89.68% 89.80% +0.11%
==========================================
Files 676 692 +16
Lines 206555 215773 +9218
Branches 39552 41299 +1747
==========================================
+ Hits 185249 193767 +8518
- Misses 13444 14118 +674
- Partials 7862 7888 +26
... and 42 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Sorry, something went wrong.
|
And why not something like OPFS aka whatwg/fs? const rootHandle = await navigator.storage.getDirectory()
await rootHandle.getFileHandle('config.json', { create: true })
fs.mount('/app', rootHandle) // to make it work with fs
fs.readFileSync('/app/config.json')OR const rootHandle = await navigator.storage.getDirectory()
await rootHandle.getFileHandle('config.json', { create: true })
fs.readFileSync('sandbox:/config.json')fs.createVirtual seems like something like a competing specification |
Sorry, something went wrong.
I generally prefer not to interleave with WHATWG specs as much as possible for core functionality (e.g., SEA). In my experience, they tend to perform poorly on our codebase and remove a few degrees of flexibility. (I also don't find much fun in working on them, and I'm way less interested in contributing to that.) On an implementation side, the core functionality of this feature will be identical (technically, it's missing writes that OPFS supports), as we would need to impact all our internal fs methods anyway. If this lands, we can certainly iterate on a WHATWG-compatible API for this, but I would not add this to this PR. |
Sorry, something went wrong.
|
Small prior art: https://github.com/juliangruber/subfs |
Sorry, something went wrong.
|
I also worked on this a bit on the side recently: Qard@73b8fc6 That is very much in chaotic ideation stage with a bunch of LLM assistance to try some different ideas, but the broader concept I was aiming for was to have a VirtualFileSystem type which would actually implement the entire API surface of the fs module, accepting a Provider type to delegate the internals of the whole cluster of file system types to a singular class managing the entire cluster of fs-related types such that the fs module could actually just be fully converted to: module.exports = new VirtualFileSystem(new LocalProvider())I intended for it to be extensible for a bunch of different interesting scenarios, so there's also an S3 provider and a zip file provider there, mainly just to validate that the model can be applied to other varieties of storage systems effectively. Keep in mind, like I said, the current state is very much just ideation in a branch I pushed up just now to share, but I think there are concepts for extensibility in there that we could consider to enable a whole ecosystem of flexible storage providers. 🙂 Personally, I would hope for something which could provide both read and write access through an abstraction with swappable backends of some variety, this way we could pass around these virtualized file systems like objects and let an ecosystem grow around accepting any generalized virtual file system for its storage backing. I think it'd be very nice for a lot of use cases like file uploads or archive management to be able to just treat them like any other readable and writable file system. |
Sorry, something went wrong.
just a bit off topic... but this reminds me of why i created this feature request: Would not lie, it would be cool if NodeJS also provided some type of static Blob.from function to create virtual lazy blobs. could live on fs.blobFrom for now... example that would only work in NodeJS (based on how it works internally) const size = 26
const blobPart = BlobFrom({
size,
stream (start, end) {
// can either be sync or async (that resolves to a ReadableStream)
// return new Response('abcdefghijklmnopqrstuvwxyz'.slice(start, end)).body
// return new Blob(['abcdefghijklmnopqrstuvwxyz'.slice(start, end)]).stream()
return fetch('https://httpbin.dev/range/' + size, {
headers: {
range: `bytes=${start}-${end - 1}`
}
}).then(r => r.body)
}
})
blobPart.text().then(text => {
console.log('a-z', text)
})
blobPart.slice(-3).text().then(text => {
console.log('x-z', text)
})
const a = blobPart.slice(0, 6)
a.text().then(text => {
console.log('a-f', text)
})
const b = a.slice(2, 4)
b.text().then(text => {
console.log('c-d', text)
})x-z xyz a-z abcdefghijklmnopqrstuvwxyz a-f abcdef c-d cd An actual working PoC(I would not rely on this unless it became officially supported by nodejs core - this is a hack) const blob = new Blob()
const symbols = Object.getOwnPropertySymbols(blob)
const blobSymbol = symbols.map(s => [s.description, s])
const symbolMap = Object.fromEntries(blobSymbol)
const {
kHandle,
kLength,
} = symbolMap
function BlobFrom ({ size, stream }) {
const blob = new Blob()
if (size === 0) return blob
blob[kLength] = size
blob[kHandle] = {
span: [0, size],
getReader () {
const [start, end] = this.span
if (start === end) {
return { pull: cb => cb(0) }
}
let reader
return {
async pull (cb) {
reader ??= (await stream(start, end)).getReader()
const {done, value} = await reader.read()
cb(done ^ 1, value)
}
}
},
slice (start, end) {
const [baseStart] = this.span
return {
span: [baseStart + start, baseStart + end],
getReader: this.getReader,
slice: this.slice,
}
}
}
return blob
}currently problematic to do: new Blob([a, b]), new File([blobPart], 'alphabet.txt', { type: 'text/plain' }) also need to handle properly clone, serialize & deserialize, if this where to be sent of to another worker - then i would transfer a MessageChannel where the worker thread asks main frame to hand back a transferable ReadableStream when it needs to read something. but there are probably better ways to handle this internally in core with piping data directly to and from different destinations without having to touch the js runtime? - if only getReader could return the reader directly instead of needing to read from the ReadableStream using js? |
Sorry, something went wrong.
| const kEntry = Symbol('kEntry'); | ||
|
|
||
| // FD range: 10000+ to avoid conflicts with real fds | ||
| let nextFd = 10_000; |
There was a problem hiding this comment.
Linux will happily issue you file descriptors after this number, without even trying to hard or messing with sysctl knobs. I assume the same is true for other Unixes.
I'm confused why it was chosen. I only looked through to here because I saw the note in the PR description. Was curious to see if there was more explanation in this file.
Maybe use negative numbers for virtual fds?
Sorry, something went wrong.
There was a problem hiding this comment.
Probably Claude saw that was the default limit on macOS and ran with it. 🤷🏻
Sorry, something went wrong.
There was a problem hiding this comment.
I can revive @jasnell idea of using negative values. Many tests were failing, but it's something we could try.
We could also start allocating from a higher range.
Sorry, something went wrong.
There was a problem hiding this comment.
Probably Claude saw that was the default limit on macOS and ran with it. 🤷🏻
That is actually the limit I have on the Linux box I'm using.
Sorry, something went wrong.
There was a problem hiding this comment.
I can revive @jasnell idea of using negative values. Many tests were failing, but it's something we could try.
I'd recommend against that. WASM-compiled softwares will often assume that open returning negative numbers means an error happened (example).
Instead consider using the second upper bit; that's what we've been doing in fslib and it has reliably worked:
https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-fslib/sources/MountFS.ts#L12-L19
Sorry, something went wrong.
There was a problem hiding this comment.
Thanks!
Sorry, something went wrong.
There was a problem hiding this comment.
agreed. what might be worthwhile is making the range configurable. >= 10,000 is a reasonable default but allow it to be configured.
Sorry, something went wrong.
There was a problem hiding this comment.
The second upper bit seems awesome. But, if it and the other methods don't work out, another thing to try would be growing down.
Starting at some very large number and grow downwards further mitigates reaching into plausible file descriptors.
Sorry, something went wrong.
VFS file descriptors now use bit 30 (0x40000000) to distinguish them from real OS file descriptors. This avoids any possibility of collision with real fds while keeping VFS fds as valid positive integers, which is required by unix conventions. Inspired by Yarn's MountFS approach.
There was a problem hiding this comment.
Thanks for the work, Matteo. I am not sure if I understand the point of per-VFS working directories, especially if this mechanism is also going to hook into process.chdir(). I think it'd be reasonable to only allow path resolution relative to the virtual root within a VFS. The interaction between this feature and child processes, which inherit the Node.js working directory, also does not seem to have a clear solution.
Sorry, something went wrong.
| ### Native addons | ||
|
|
||
| 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. |
There was a problem hiding this comment.
This section should probably also mention that native addons, like child processes, also cannot see virtual file systems.
Sorry, something went wrong.
| If case-insensitive matching is required, applications should normalize paths | ||
| before VFS operations. |
There was a problem hiding this comment.
Normalizing paths might not resolve this issue if the application is not responsible for creating the file.
Sorry, something went wrong.
There was a problem hiding this comment.
can you make an example?
Sorry, something went wrong.
| * **Read operations** (`readFile`, `readdir`, `stat`, `lstat`, `access`, | ||
| `exists`, `realpath`, `readlink`, `statfs`, `opendir`): 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`, `truncate`, `link`, `chmod`, `chown`, | ||
| `utimes`, `lutimes`, `mkdtemp`, `rm`, `cp`): 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. |
There was a problem hiding this comment.
Does this mean that the behavior of open() depends on the flags that are specified? Is the idea that all write operations (even, for example, O_RDWR without O_CREAT) target the VFS only, so the underlying filesystem should not be modifiable through this?
Sorry, something went wrong.
There was a problem hiding this comment.
That was my initial thought as well. But it seems that you could mount that to a real file system. According to: #62328 (comment)
Sorry, something went wrong.
There was a problem hiding this comment.
Does this mean that the behavior of open() depends on the flags that are specified? Is the idea that all write operations (even, for example, O_RDWR without O_CREAT) target the VFS only, so the underlying filesystem should not be modifiable through this?
A VFS provider can implement those flags as they would please, e.g. S3 operations. In the current codebase, those are passed through the real provider.
Sorry, something went wrong.
I would like to be able to run a Node.js application inside worker threads, and some of them, unfortunately, do chdir() within dependencies. Supporting chdir() will allow us to do it. |
Sorry, something went wrong.
Co-authored-by: Tobias Nießen <tniessen@tnie.de>
Thanks Matteo, I understand the motivation now. Personally, I don't feel great about retrofitting chdir() to not actually call the sys_chdir() syscall in some cases because it will be tricky to get child processes to inherit the property in a POSIX-compliant manner, especially if a child process is launched from a native addon or so. |
Sorry, something went wrong.
The same can be said of all the content in the VFS and native addons. Native addons won't be able to "see" inside the vfs. |
Sorry, something went wrong.
|
My concern is not strictly limited to VFS; any chdir() implementation that does not actually dispatch the SYS_chdir syscall to the kernel could violate POSIX assumptions without extra precautions, regardless of whether the path exists in the real file system or not. |
Sorry, something went wrong.
|
I've extracted #63115 from this PR. It includes only the addition and no integration points. |
Sorry, something went wrong.
There was a problem hiding this comment.
Thanks for the explanatory comment
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
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:
Standard fs API - Uses familiar writeFileSync, readFileSync, mkdirSync instead of custom methods
Mount Mode - VFS mounts at a specific path prefix (e.g., /virtual), clear separation from real filesystem
Module Loading - require() and import work seamlessly from virtual files
SEA Integration - Assets automatically mounted at /sea when running as a Single Executable Application
Full 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 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:
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.
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).
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