Skip to content

Conversation

@mcollina
Copy link
Member

@mcollina mcollina commented Jan 22, 2026

A first-class virtual file system that integrates with Node.js's fs module and module loader.

Key Features

  • fs.createVirtual() - Create isolated virtual file systems with files and directories
  • Mount mode - VFS active only under a specific path prefix (e.g., /app)
  • Overlay mode - VFS checked first for all paths, falls through to real fs
  • Module loading - require() and import work seamlessly from virtual files
  • Dynamic content - Files can be functions that generate content on each read
  • Full fs API support - readFileSync, statSync, readdirSync, existsSync, streams, promises, glob

##Integrations

  • SEA (Single Executable Application) - fs.getSeaVfs() auto-populates VFS from embedded assets
  • Test Runner - t.mock.fs() creates isolated mock file systems for testing with automatic cleanup

Example

const fs = require('fs');

const vfs = fs.createVirtual();
vfs.addFile('/config.json', '{"debug": true}');
vfs.mount('/app');

// Works with standard fs APIs
const config = JSON.parse(fs.readFileSync('/app/config.json', 'utf8'));
const mod = require('/app/module.js');

Disclaimer: I've used a significant amount of Claude Code tokens to create this PR. I've reviewed all changes myself.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/single-executable
  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Jan 22, 2026
@avivkeller avivkeller added fs Issues and PRs related to the fs subsystem / file system. module Issues and PRs related to the module subsystem. semver-minor PRs that contain new features and should be released in the next minor version. notable-change PRs with changes that should be highlighted in changelogs. needs-benchmark-ci PR that need a benchmark CI run. test_runner Issues and PRs related to the test runner subsystem. labels Jan 22, 2026
@github-actions
Copy link
Contributor

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.

@Ethan-Arrowood
Copy link
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.

*/
existsSync(path) {
// Prepend prefix to path for VFS lookup
const fullPath = this.#prefix + (StringPrototypeStartsWith(path, '/') ? path : '/' + path);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use path.join?

Comment on lines +417 to +435
/**
* Gets the underlying VirtualFileSystem instance.
* @returns {VirtualFileSystem}
*/
get vfs() {
return this.#vfs;
}

/**
* Gets the mount prefix for the mock file system.
* @returns {string}
*/
get prefix() {
return this.#prefix;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be getters? Why can't we expose the actual values.

If a user overwrites them, they can if they wish?

validateObject(files, 'options.files');
}

const { VirtualFileSystem } = require('internal/vfs/virtual_fs');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we import this at the top level / lazy load it at the top level?

ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreFS,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
restore: restoreFS,
restore: ctx.restore,

nit

* @param {object} [options] Optional configuration
*/
addFile(name, content, options) {
const path = this._directory.path + '/' + name;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use path.join?

let entry = current.getEntry(segment);
if (!entry) {
// Auto-create parent directory
const dirPath = '/' + segments.slice(0, i + 1).join('/');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use path.join

let entry = current.getEntry(segment);
if (!entry) {
// Auto-create parent directory
const parentPath = '/' + segments.slice(0, i + 1).join('/');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path.join?

}
}
callback(null, content);
}).catch((err) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}).catch((err) => {
}, (err) => {

Comment on lines 676 to 677
const bytesToRead = Math.min(length, available);
content.copy(buffer, offset, readPos, readPos + bytesToRead);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primordials?

}

callback(null, bytesToRead, buffer);
}).catch((err) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}).catch((err) => {
}, (err) => {

@avivkeller
Copy link
Member

Left an initial review, but like @Ethan-Arrowood said, it'll take time for a more in depth look

@joyeecheung
Copy link
Member

joyeecheung commented Jan 22, 2026

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.

@codecov
Copy link

codecov bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 88.90524% with 377 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.93%. Comparing base (637bda0) to head (8d711c1).
⚠️ Report is 57 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/vfs/virtual_fs.js 91.09% 117 Missing and 3 partials ⚠️
lib/internal/vfs/module_hooks.js 80.16% 118 Missing and 1 partial ⚠️
lib/internal/main/embedding.js 60.00% 24 Missing ⚠️
lib/internal/vfs/entries.js 93.44% 23 Missing ⚠️
lib/internal/vfs/sea.js 77.65% 21 Missing ⚠️
lib/internal/vfs/streams.js 88.19% 19 Missing ⚠️
lib/internal/vfs/errors.js 88.53% 18 Missing ⚠️
lib/internal/vfs/router.js 86.66% 17 Missing and 1 partial ⚠️
lib/internal/vfs/fd.js 94.57% 9 Missing ⚠️
lib/internal/vfs/stats.js 96.93% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61478      +/-   ##
==========================================
+ Coverage   88.51%   88.93%   +0.42%     
==========================================
  Files         704      676      -28     
  Lines      208883   207079    -1804     
  Branches    40334    39560     -774     
==========================================
- Hits       184890   184163     -727     
+ Misses      15977    15209     -768     
+ Partials     8016     7707     -309     
Files with missing lines Coverage Δ
lib/fs.js 93.36% <100.00%> (-4.84%) ⬇️
lib/internal/test_runner/mock/mock.js 98.86% <100.00%> (+0.13%) ⬆️
lib/sea.js 97.93% <100.00%> (-0.96%) ⬇️
lib/internal/vfs/stats.js 96.93% <96.93%> (ø)
lib/internal/vfs/fd.js 94.57% <94.57%> (ø)
lib/internal/vfs/errors.js 88.53% <88.53%> (ø)
lib/internal/vfs/router.js 86.66% <86.66%> (ø)
lib/internal/vfs/streams.js 88.19% <88.19%> (ø)
lib/internal/vfs/sea.js 77.65% <77.65%> (ø)
lib/internal/vfs/entries.js 93.44% <93.44%> (ø)
... and 3 more

... and 174 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jimmywarting
Copy link

jimmywarting commented Jan 22, 2026

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

@mcollina mcollina force-pushed the vfs branch 2 times, most recently from f1cb562 to 5e317de Compare January 23, 2026 06:21
Add a read-only virtual file system (VFS) that can be mounted at a
specific path prefix, enabling standard fs APIs to work transparently
with in-memory files.

Key features:
- fs.createVirtual() to create VFS instances
- Support for files, directories, and symbolic links
- Full async/sync/promise API support (readFile, stat, readdir, etc.)
- File descriptor operations (open, read, close)
- createReadStream() support
- fs.glob() integration
- CJS require() and ESM import() support via module hooks
- Virtual process.chdir() for relative path resolution
- SEA integration via sea.getVfs() and sea.hasAssets()
- Test runner mock.fs() for file system mocking

The VFS is read-only by design and uses virtual file descriptors
(10000+) to avoid conflicts with real file descriptors.
@mcollina
Copy link
Member Author

And why not something like OPFS aka whatwg/fs?

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.

@juliangruber
Copy link
Member

Small prior art: https://github.com/juliangruber/subfs

- Use path.isAbsolute() for cross-platform absolute path detection
  instead of checking for '/' prefix only
- Use path.posix.join() in createScopedVFS to ensure forward slashes
  on all platforms since VFS uses '/' internally
- Fix ESM module hook to recognize Windows absolute paths (C:\)
- Fix symlink target resolution for Windows paths

This enables VFS to work correctly on Windows where absolute paths
use drive letters (e.g., C:\path) instead of Unix-style forward
slashes.
Enable direct require() of modules from VFS in Single Executable
Applications. Previously, SEA's embedderRequire only supported built-in
modules, requiring users to use module.createRequire() for VFS paths.

Now, after calling sea.getVfs(), you can require VFS modules directly:

```js
const sea = require('node:sea');
sea.getVfs(); // Initialize VFS
const myModule = require('/sea/lib/mymodule.js');
```

The implementation:
- Lazily initializes SEA VFS on first non-builtin require
- Checks if the required path exists in VFS
- Uses Module._load to load from VFS via the registered hooks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fs Issues and PRs related to the fs subsystem / file system. lib / src Issues and PRs related to general changes in the lib or src directory. module Issues and PRs related to the module subsystem. needs-benchmark-ci PR that need a benchmark CI run. needs-ci PRs that need a full CI run. notable-change PRs with changes that should be highlighted in changelogs. semver-minor PRs that contain new features and should be released in the next minor version. test_runner Issues and PRs related to the test runner subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants