Skip to content

Virtual File System for Node.js#61478

Draft
mcollina wants to merge 125 commits intonodejs:mainfrom
mcollina:vfs
Draft

Virtual File System for Node.js#61478
mcollina wants to merge 125 commits intonodejs:mainfrom
mcollina:vfs

Conversation

@mcollina
Copy link
Member

@mcollina mcollina commented Jan 22, 2026

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 support
    • SEAProvider - Read-only access to Single Executable Application assets
    • VirtualProvider - Base class for creating custom 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

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.


Fixes #60021

@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?

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 92.99967% with 635 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.82%. Comparing base (e0928d6) to head (b047092).
⚠️ Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/vfs/setup.js 82.25% 223 Missing and 1 partial ⚠️
lib/internal/vfs/providers/memory.js 89.87% 99 Missing and 3 partials ⚠️
lib/internal/vfs/providers/real.js 84.59% 65 Missing ⚠️
lib/internal/vfs/file_system.js 96.16% 53 Missing ⚠️
lib/internal/vfs/watcher.js 92.93% 43 Missing and 3 partials ⚠️
lib/internal/vfs/streams.js 90.61% 29 Missing ⚠️
lib/internal/vfs/stats.js 91.34% 21 Missing and 4 partials ⚠️
lib/internal/vfs/provider.js 96.11% 20 Missing and 4 partials ⚠️
src/node_sea.cc 64.28% 12 Missing and 8 partials ⚠️
lib/internal/fs/cp/cp-sync.js 63.41% 15 Missing ⚠️
... and 9 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61478      +/-   ##
==========================================
+ Coverage   89.68%   89.82%   +0.13%     
==========================================
  Files         676      692      +16     
  Lines      206555   215687    +9132     
  Branches    39552    41259    +1707     
==========================================
+ Hits       185249   193739    +8490     
- Misses      13444    14061     +617     
- Partials     7862     7887      +25     
Files with missing lines Coverage Δ
lib/fs.js 98.54% <100.00%> (+0.35%) ⬆️
lib/internal/bootstrap/realm.js 96.21% <100.00%> (+<0.01%) ⬆️
lib/internal/fs/utils.js 99.68% <100.00%> (+<0.01%) ⬆️
lib/internal/modules/cjs/loader.js 98.20% <100.00%> (+0.05%) ⬆️
lib/internal/modules/esm/get_format.js 94.83% <100.00%> (ø)
lib/internal/modules/esm/load.js 91.47% <100.00%> (ø)
lib/internal/modules/esm/resolve.js 99.03% <100.00%> (-0.01%) ⬇️
lib/internal/modules/esm/translators.js 97.67% <100.00%> (+<0.01%) ⬆️
lib/internal/modules/helpers.js 98.73% <100.00%> (+0.01%) ⬆️
lib/internal/modules/package_json_reader.js 99.72% <100.00%> (+<0.01%) ⬆️
... and 26 more

... and 42 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 3 times, most recently from 5e317de to 977cc3d Compare January 23, 2026 08:15
@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

@mcollina mcollina force-pushed the vfs branch 2 times, most recently from 8d711c1 to 73c18cd Compare January 23, 2026 13:19
@Qard
Copy link
Member

Qard commented Jan 23, 2026

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.

@jimmywarting
Copy link

jimmywarting commented Jan 23, 2026

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.

just a bit off topic... but this reminds me of why i created this feature request:
Blob.from() for creating virtual Blobs with custom backing storage

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?

@philipwhiuk

This comment has been minimized.

Switch MemoryFileHandle from exact-fit allocation to geometric
doubling (capacity * 2) for amortized O(1) append performance.
Track actual content size separately from buffer capacity via
a #size field, and expose only valid data through subarray views.
@joyeecheung
Copy link
Member

Can folks chiming in on the meta discussions about AI/LLM move to #62105 or a separate thread about this topic specifically? GitHub doesn’t handle long threads very well. This PR is already quite difficult to track for technical reviews, please keep the discussions here specific to this PR (question and answers about the copyright analysis are okay as they are technically specific to the content of this PR; more meta discussions about tools can use a dedicated space elsewhere or they will quickly be buried in GitHub’s thread folding along with technical reviews).

@jasnell
Copy link
Member

jasnell commented Mar 18, 2026

I've marked my analysis comment as resolved as I believe it made the point I was trying to make. For the remaining technical discussions, I really think it's going to be easier to do incremental follow-ups than it will be to continue refining this beast of a PR. But, agreed... The rest of the discussion should be focused on the technical side of things

@paulshryock

This comment was marked as spam.

@BLamy
Copy link

BLamy commented Mar 18, 2026

are there any plans to support command execution inside the vfs? like

const vfs = require('node:vfs');
const fs = require('node:fs');

// Create a VFS with default MemoryProvider
const myVfs = vfs.create();

myVfs.exec('ls /')
myVfs.spwan('npm i')

@ovflowd
Copy link
Member

ovflowd commented Mar 18, 2026

I'm locking this PR on the grounds that it is becoming off-topic and diverging from technical discussions. This is an impartial lock to ensure this PR is on-track.

Questions and concerns regarding AI-practices directly related to this PR or not, please follow to the linked issue on OpenJS Foundation

On a separate note, and not connected to the lock, I fully agree with @jasnell on all his grounds and do believe that the elusive assertion of the DCO not being fullfiled should be prompted elsewhere instead of directly on this PR. OpenJS Foundation Legal cleared the DCO is correctly being followed, so any further deliberation on the matter won't be useful for the development of this PR.

Thank you!

Copy link
Contributor

@Ethan-Arrowood Ethan-Arrowood left a comment

Choose a reason for hiding this comment

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

Really impressed with this work. I particularly focussed my review on the docs and tests. I'm very happy with the shape of the API and am excited for this feature. Excellent! 🚀

Copy link
Member

@jasnell jasnell left a comment

Choose a reason for hiding this comment

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

Flagging a handful of nits to follow-up on later. More extensive follow-up review here: #62328

I consider these non-blocking for this PR to land.

if (h !== null) {
const vfsResult = await h.readdir(path, options);
if (vfsResult !== undefined) return vfsResult;
}
Copy link
Member

Choose a reason for hiding this comment

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

Follow up for later: there's a fair amount of duplication in these checks... using a shared utility would reduce the duplication.

* @param {string|number} flags The flags to normalize
* @returns {string} Normalized string flags
*/
function normalizeFlags(flags) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I'm sure we have this implemented somewhere else already. In a follow up this should be de-duplicated.

flags === 'ax' || flags === 'ax+';
const isExclusive = flags === 'wx' || flags === 'wx+' ||
flags === 'ax' || flags === 'ax+';
const isWritable = flags !== 'r';
Copy link
Member

Choose a reason for hiding this comment

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

Likewise... we should have similar logic elsewhere already... in a follow up this should be deduplicated

Comment on lines +640 to +641
entry = new MemoryEntry(TYPE_DIR, { mode: options?.mode });
entry.children = new SafeMap();
Copy link
Member

Choose a reason for hiding this comment

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

Nit: for follow-up ... is there ever a case where entry.children should not just be initialized to a new SafeMap on construction?


async open(vfsPath, flags, mode) {
const realPath = this.#resolvePath(vfsPath);
return new Promise((resolve, reject) => {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: for follow-up, would prefer using PromiseWithResolvers to replace the new Promise pattern. It avoids allocating the additional closure.


statSync(vfsPath, options) {
const realPath = this.#resolvePath(vfsPath);
return fs.statSync(realPath, options);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: for follow-up consideration... should these be using the public API? These can be monkeypatched by user-land and end up with very different implementations.

const bytesToRead = MathMin(length, available);
this.#content.copy(buffer, offset, readPos, readPos + bytesToRead);

if (position === null || position === undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: for follow-up position == null

// Create a copy so the returned buffer is independently mutable.
// Buffer.from(ArrayBuffer) creates a view sharing the same memory,
// which may be backed by read-only segments in the executable.
return Buffer.from(new Uint8Array(content));
Copy link
Member

Choose a reason for hiding this comment

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

Nit: what type is content here? This should be documented.

there's a risk of double copy here depending on what content is.

@Renegade334
Copy link
Member

Should this live behind a CLI flag initially? It might be better to work through #62328 while the API is not exposed by default.

@jasnell
Copy link
Member

jasnell commented Mar 18, 2026

Should this live behind a CLI flag initially?

Honestly... wouldn't hurt.

@mcollina
Copy link
Member Author

mcollina commented Mar 18, 2026

Should this live behind a CLI flag initially? It might be better to work through #62328 while the API is not exposed by default.

The biggest risk of this PR is the patches on the modules' loading side and the fs side. You cannot put them behind a flag. The rest is just a new API with bugs (like many others).

Add 18 feature-focused test files covering bug fixes from both analysis
rounds, and fix lint issues in stats.js and setup.js.

Test files added:
- test-vfs-access.js: access validation and mode enforcement
- test-vfs-buffer-encoding.js: buffer encodings and buffer path args
- test-vfs-copyfile-mode.js: copyFile COPYFILE_EXCL support
- test-vfs-dir-handle.js: Dir double-close and read/close callbacks
- test-vfs-file-url.js: file: URL handling with fileURLToPath
- test-vfs-mkdir-recursive-return.js: mkdirSync recursive return value
- test-vfs-no-auto-mkdir.js: writes/opens don't auto-create parents
- test-vfs-open-flags.js: read-only/write-only/exclusive/numeric flags
- test-vfs-readdir-recursive.js: readdir recursive with names and dirents
- test-vfs-readfile-fd.js: readFileSync with virtual fd
- test-vfs-rename-safety.js: renameSync preserves source on failure
- test-vfs-rm-edge-cases.js: rmSync dir/link edge cases
- test-vfs-stats-bigint.js: BigInt stats support
- test-vfs-stream-options.js: writestream start and stream fd option
- test-vfs-symlink-edge-cases.js: broken/intermediate symlinks
- test-vfs-watch-directory.js: directory and recursive watch
- test-vfs-watchfile.js: unwatchFile cleanup and zero stats
- test-vfs-writefile-flags.js: writeFile/appendFile flag support

Lint fixes:
- stats.js: use BigInt from primordials, add missing JSDoc @returns
- setup.js: consolidate ERR_MODULE_NOT_FOUND import
@ljharb
Copy link
Member

ljharb commented Mar 18, 2026

Could those patches land separately from the flaggable part? That way it’d be much easier to ensure no regressions from either portion.

(i ofc wouldn’t suggest splitting the PR until after it was otherwise philosophically landable)

@mcollina
Copy link
Member Author

@ljharb maybe. But we would be landing code that is not tested, so we’d be in a worse stance?

Note that experimental features are covered by our threat model.

@ljharb
Copy link
Member

ljharb commented Mar 18, 2026

Surely either the patches are meant to be noops in isolation (in which case existing tests should be sufficient - or improved - to assure no breakage) or have independent functionality (which could ship with its own tests)?

@mcollina
Copy link
Member Author

I don’t understand why. Ease of review?

@ljharb
Copy link
Member

ljharb commented Mar 18, 2026

Ease of review, and if there’s any risk in patching core subsystems, that PR could also be released as a patch and then that assumption would be tested, and the VFS functionality could land separately as a minor.

Certainly not required, but it might be prudent for assuring stability.

- Check AbortSignal before VFS fast path in readFile/writeFile/appendFile
- Honor options.flag in readFile/readFileSync on VFS paths
- Route realpath.native/realpathSync.native through VFS handlers
- Implement chmod/chown/utimes/lutimes in MemoryProvider instead of no-ops
- Pass bigint option through VFSStatWatcher to statSync/createZeroStats
- Coerce BigInt positions to Number in MemoryFileHandle read/write
- Track nlink on MemoryEntry, increment on link, decrement on unlink
- Update ctime alongside mtime on content mutations (write/truncate)
- Throw ERR_INVALID_PACKAGE_CONFIG for malformed package.json in VFS
  instead of silently falling through to index.js resolution
- Move writeFile/appendFile options validation (getOptions, parseFileMode,
  validateBoolean) before VFS fast path so invalid options are rejected
- Validate flags with stringToFlags() before VFS check in open/openSync
  so invalid flag values like {} throw ERR_INVALID_ARG_VALUE
- Fix rmdirSync to not follow symlinks (use getEntry with false) so
  symlinks to directories correctly throw ENOTDIR
- Update parent directory mtime/ctime when children are added or removed
  in openSync, mkdirSync, rmdirSync, unlinkSync, linkSync, symlinkSync,
  and renameSync
- Pass bigint option through to VFS statfs handlers and return BigInt
  values when options.bigint is true
Convert VFS callback and promise code paths from calling sync handler
methods to async handlers that return undefined (not handled) or a
Promise (VFS handles it). This removes sync method calls from async
code paths, making VFS safe for custom providers that do real I/O.

Add DRY utilities (vfsRead, vfsOp, vfsOpVoid in setup.js and
vfsResult, vfsVoid in fs.js) to eliminate repeated boilerplate across
~80 handler methods and ~40 fs functions. Add missing promises methods
(chmod, chown, lchown, utimes, lutimes, open, lchmod) to
file_system.js. Fix bugs where chown/lchown/lutimes async handlers
called wrong sync methods. Fix invalid package.json handling to
gracefully fall through in CJS context instead of throwing.
@mcollina
Copy link
Member Author

I’m moving this back to draft. I’m doing some refactoring and simplifying some paths for ease of review. I’ll add a review guide in the PR description and an architecture diagram explaining how all pieces fit together.

Convert FD-based callback functions (close, read, write, fstat,
ftruncate, fdatasync, fsync, fchmod, fchown, futimes, readv, writev)
from sync handler + process.nextTick to async handlers using the
undefined | Promise pattern, matching the approach already used for
path-based operations.

Add async FD handlers to setup.js that call the async methods on
MemoryFileHandle (read, write, stat, truncate, close) instead of
their sync counterparts, avoiding event loop blocking for custom
VFS providers that do real I/O.

Fix vfs.md documentation that was significantly out of date:
- Remove false claim that chmod, chown, truncate, utimes, link,
  fdatasync, fsync have no VFS equivalent (all are implemented)
- Add missing intercepted methods to the fs integration section
  (truncate, link, chmod, chown, lchown, utimes, lutimes, mkdtemp,
  lchmod, cp, statfs, opendir, readv, writev, ftruncate, fchmod,
  fchown, futimes, fdatasync, fsync)
- Shrink "not intercepted" list to just glob/globSync
- Add missing provider.supportsWatch documentation
- Update overlay mode operation routing lists
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. tsc-agenda Issues and PRs to discuss during the meetings of the TSC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement VFS (Virtual File System) Hooks for Single Executable Applications