Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9b99d3c
vfs: add minimal node:vfs subsystem
mcollina Apr 30, 2026
eef3609
test: add VFS API tests adapted from mount-based tests
mcollina Apr 30, 2026
da9cfcb
test: add VFS unit tests for VirtualDir, file handles, and provider base
mcollina May 1, 2026
df11b9d
test: adapt more VFS tests to direct API
mcollina May 1, 2026
ddd29c5
test: add VFS callback, stream, watch, and real-provider async tests
mcollina May 1, 2026
29d79b6
test: cover remaining VFS API edge cases
mcollina May 1, 2026
13de9de
test: push every VFS file to >=95% line coverage
mcollina May 2, 2026
82b1aab
test: push branch coverage to 95%+
mcollina May 3, 2026
d7de760
test: rename and split VFS test files into topic-based names
mcollina May 3, 2026
2e9fb17
vfs: gate node:vfs behind --experimental-vfs flag
mcollina May 3, 2026
65d6514
vfs: fix lint errors
mcollina May 4, 2026
cf9f772
vfs: import getVirtualFd eagerly in streams
mcollina May 4, 2026
877512d
doc: trim vfs docs to match the minimal API
mcollina May 4, 2026
47e2da0
vfs: register node:vfs as an experimental builtin in CI metadata
mcollina May 5, 2026
8d5f0a7
test: stabilize test-vfs-watch-directory
mcollina May 6, 2026
6cbbda1
Apply suggestions from code review
mcollina May 7, 2026
b679b20
module: exclude node:vfs from builtinModules when flag is disabled
mcollina May 8, 2026
5d2f98a
Apply suggestions from code review
mcollina May 15, 2026
f545152
vfs: use posix paths on windows
mcollina May 15, 2026
03be3c2
Apply suggestions from code review
mcollina May 15, 2026
e2323dd
Apply suggestions from code review
mcollina May 15, 2026
b31dfa7
vfs: drop RealFileHandle.fd getter
mcollina May 15, 2026
37822f0
vfs: fix regressions from code-review changes
mcollina May 16, 2026
19022a5
fixup typo
mcollina May 16, 2026
dad64ec
vfs: add TODO to reuse FileHandle for async ops
mcollina May 16, 2026
d3ae5f9
vfs: fix realpath EACCES on Windows from case canonicalization
mcollina May 17, 2026
a2a6566
Apply suggestions from code review
mcollina May 18, 2026
c6dd379
test: fix lint errors in vfs stream tests
mcollina May 18, 2026
82e79d1
Apply suggestions from code review
mcollina May 19, 2026
e105526
test: drop unused assert import in test-vfs-flag
mcollina May 19, 2026
e0a66b4
fixup
mcollina May 20, 2026
1088e5f
vfs: hoist currentPath in MemoryProvider#ensureParent
mcollina May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,16 @@ The flag may be specified more than once; tests must contain **every**
filter value to run. See [Test tags][] for details on declaring and
inheriting tags.

### `--experimental-vfs`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Enable the experimental [`node:vfs`][] module.

### `--experimental-vm-modules`

<!-- YAML
Expand Down Expand Up @@ -3786,6 +3796,7 @@ one is included in the list below.
* `--experimental-stream-iter`
* `--experimental-test-isolation`
* `--experimental-top-level-await`
* `--experimental-vfs`
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
* `--force-context-aware`
Expand Down Expand Up @@ -4428,6 +4439,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`node:ffi`]: ffi.md
[`node:sqlite`]: sqlite.md
[`node:stream/iter`]: stream_iter.md
[`node:vfs`]: vfs.md
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version
Expand Down
1 change: 1 addition & 0 deletions doc/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
* [URL](url.md)
* [Utilities](util.md)
* [V8](v8.md)
* [Virtual File System](vfs.md)
* [VM](vm.md)
* [WASI](wasi.md)
* [Web Crypto API](webcrypto.md)
Expand Down
310 changes: 310 additions & 0 deletions doc/api/vfs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# Virtual File System

<!--introduced_in=REPLACEME-->

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

<!-- source_link=lib/vfs.js -->

The `node:vfs` module provides an in-memory virtual file system with a
`node:fs`-like API. It is useful for tests, fixtures, embedded assets, and other
scenarios where you need a self-contained file system without touching the
actual file-system.

To access it:

```mjs
import vfs from 'node:vfs';
```

```cjs
const vfs = require('node:vfs');
```

This module is only available under the `node:` scheme, and only when Node.js
is started with the `--experimental-vfs` flag.

## Basic usage

```cjs
const vfs = require('node:vfs');

const myVfs = vfs.create();
myVfs.mkdirSync('/dir', { recursive: true });
myVfs.writeFileSync('/dir/hello.txt', 'Hello, VFS!');

console.log(myVfs.readFileSync('/dir/hello.txt', 'utf8')); // 'Hello, VFS!'
```

`vfs.create()` returns a [`VirtualFileSystem`][] instance backed by a
[`MemoryProvider`][] by default. The instance exposes synchronous,
callback-based, and promise-based file system methods that mirror the
shape of the [`node:fs`][] API. All paths are POSIX-style and absolute
(starting with `/`).

## `vfs.create([provider][, options])`

<!-- YAML
added: REPLACEME
-->

* `provider` {VirtualProvider} The provider to use. **Default:**
`new MemoryProvider()`.
* `options` {Object}
* `emitExperimentalWarning` {boolean} Whether to emit the experimental
warning when the instance is created. **Default:** `true`.
* Returns: {VirtualFileSystem}

Convenience factory equivalent to `new VirtualFileSystem(provider, options)`.

```cjs
const vfs = require('node:vfs');

// Default in-memory provider
const memoryVfs = vfs.create();

// Explicit provider
const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox'));
```

## Class: `VirtualFileSystem`

<!-- YAML
added: REPLACEME
-->

A `VirtualFileSystem` wraps a [`VirtualProvider`][] and exposes a
`node:fs`-like API. Each instance maintains its own file tree.

### `new VirtualFileSystem([provider][, options])`

<!-- YAML
added: REPLACEME
-->

* `provider` {VirtualProvider} The provider to use. **Default:**
`new MemoryProvider()`.
* `options` {Object}
* `emitExperimentalWarning` {boolean} Whether to emit the experimental
warning. **Default:** `true`.

### `vfs.provider`

<!-- YAML
added: REPLACEME
-->

* {VirtualProvider}

The provider backing this VFS instance.

### `vfs.readonly`

<!-- YAML
added: REPLACEME
-->

* {boolean}

`true` when the underlying provider is read-only.

### APIs

`VirtualFileSystem` implements the following methods, with the same
signatures as their [`node:fs`][] counterparts:

#### Synchronous API

* `existsSync(path)`
* `statSync(path[, options])`
* `lstatSync(path[, options])`
* `readFileSync(path[, options])`
* `writeFileSync(path, data[, options])`
* `appendFileSync(path, data[, options])`
* `readdirSync(path[, options])`
* `mkdirSync(path[, options])`
* `rmdirSync(path)`
* `unlinkSync(path)`
* `renameSync(oldPath, newPath)`
* `copyFileSync(src, dest[, mode])`
* `realpathSync(path[, options])`
* `readlinkSync(path[, options])`
* `symlinkSync(target, path[, type])`
* `accessSync(path[, mode])`
* `rmSync(path[, options])`
* `truncateSync(path[, len])`
* `ftruncateSync(fd[, len])`
* `linkSync(existingPath, newPath)`
* `chmodSync(path, mode)`
* `chownSync(path, uid, gid)`
* `utimesSync(path, atime, mtime)`
* `lutimesSync(path, atime, mtime)`
* `mkdtempSync(prefix)`
* `opendirSync(path[, options])`
* `openAsBlob(path[, options])`
* File-descriptor ops: `openSync`, `closeSync`, `readSync`, `writeSync`,
`fstatSync`
* Streams: `createReadStream`, `createWriteStream`
* Watchers: `watch`, `watchFile`, `unwatchFile`

#### Callback API

`readFile`, `writeFile`, `stat`, `lstat`, `readdir`, `realpath`, `readlink`,
`access`, `open`, `close`, `read`, `write`, `rm`, `fstat`, `truncate`,
`ftruncate`, `link`, `mkdtemp`, `opendir`. Each takes a Node.js-style
callback `(err, ...result) => {}`.

#### Promise API

`vfs.promises` exposes the promise-based variants:

```cjs
const vfs = require('node:vfs');

async function example() {
const myVfs = vfs.create();
await myVfs.promises.writeFile('/file.txt', 'hello');
const data = await myVfs.promises.readFile('/file.txt', 'utf8');
return data;
}
example();
```

The promise namespace mirrors `fs.promises` and includes `readFile`,
`writeFile`, `appendFile`, `stat`, `lstat`, `readdir`, `mkdir`, `rmdir`,
`unlink`, `rename`, `copyFile`, `realpath`, `readlink`, `symlink`,
`access`, `rm`, `truncate`, `link`, `mkdtemp`, `chmod`, `chown`, `lchown`,
`utimes`, `lutimes`, `open`, `lchmod`, and `watch`.

## Class: `VirtualProvider`

<!-- YAML
added: REPLACEME
-->

The base class for all VFS providers. Subclasses implement the essential
primitives (`open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`,
`rename`, ...) and inherit default implementations of the derived
The base class for all VFS providers. Subclasses implement the essential
primitives (such as `open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`,
`rename`, etc.) and inherit default implementations of the derived
methods (such as `readFile`, `writeFile`, `exists`, `copyFile`, `access`, etc.).

### Capability flags

* `provider.readonly` {boolean} **Default:** `false`.
* `provider.supportsSymlinks` {boolean} **Default:** `false`.
* `provider.supportsWatch` {boolean} **Default:** `false`.

### Creating custom providers

```cjs
const { VirtualProvider } = require('node:vfs');

class StaticProvider extends VirtualProvider {
get readonly() { return true; }

statSync(path) { /* ... */ }
openSync(path, flags) { /* ... */ }
readdirSync(path, options) { /* ... */ }
// ...
}
```

The base class throws `ERR_METHOD_NOT_IMPLEMENTED` for any primitive
that has not been overridden, and rejects writes from a `readonly`
provider with `EROFS`.

## Class: `MemoryProvider`

<!-- YAML
added: REPLACEME
-->

The default in-memory provider. Stores files, directories, and symbolic
links in a `Map`-backed tree, supports symlinks (`supportsSymlinks ===
true`), and supports watching (`supportsWatch === true`).

### `memoryProvider.setReadOnly()`

<!-- YAML
added: REPLACEME
-->

Locks the provider into read-only mode. Subsequent writes through any
[`VirtualFileSystem`][] using this provider throw `EROFS`. There is no
way to revert the provider to writable.

```cjs
const vfs = require('node:vfs');

const provider = new vfs.MemoryProvider();
const myVfs = vfs.create(provider);
myVfs.writeFileSync('/seed.txt', 'initial');

provider.setReadOnly();

myVfs.writeFileSync('/x.txt', 'fail'); // throws EROFS
```

## Class: `RealFSProvider`

<!-- YAML
added: REPLACEME
-->

A provider that wraps a directory (i.e. one on the actual file system) and exposes its
contents through the VFS API. All VFS paths are resolved relative to
the root and verified to stay inside it; symbolic links resolving
outside the root are rejected.

### `new RealFSProvider(rootPath)`

<!-- YAML
added: REPLACEME
-->

* `rootPath` {string} The absolute file-system path to use as the root.
Must be a non-empty string.

```cjs
const vfs = require('node:vfs');

const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox'));
realVfs.writeFileSync('/file.txt', 'hello'); // writes /tmp/sandbox/file.txt
```

### `realFSProvider.rootPath`

<!-- YAML
added: REPLACEME
-->

* {string}

The resolved absolute path used as the root.

## Implementation details

### `Stats` objects

VFS `Stats` objects are real instances of [`fs.Stats`][] (or
[`fs.BigIntStats`][] when `{ bigint: true }` is requested). Their
fields use synthetic but stable values:

* `dev` is `4085` (the VFS device id).
* `ino` is monotonically increasing per process.
* `blksize` is `4096`.
* `blocks` is `Math.ceil(size / 512)`.
* Times default to the moment the entry was created/last modified.

[`MemoryProvider`]: #class-memoryprovider
[`VirtualFileSystem`]: #class-virtualfilesystem
[`VirtualProvider`]: #class-virtualprovider
[`fs.BigIntStats`]: fs.md#class-fsbigintstats
[`fs.Stats`]: fs.md#class-fsstats
[`node:fs`]: fs.md
7 changes: 7 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,11 @@ Enable the experimental
.Sy node:stream/iter
module.
.
.It Fl -experimental-vfs
Enable the experimental
.Sy node:vfs
module.
.
.It Fl -experimental-sea-config
Use this flag to generate a blob that can be injected into the Node.js
binary to produce a single executable application. See the documentation
Expand Down Expand Up @@ -1945,6 +1950,8 @@ one is included in the list below.
.It
\fB--experimental-top-level-await\fR
.It
\fB--experimental-vfs\fR
.It
\fB--experimental-vm-modules\fR
.It
\fB--experimental-wasi-unstable-preview1\fR
Expand Down
11 changes: 10 additions & 1 deletion lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,18 @@ const schemelessBlockList = new SafeSet([
'quic',
'test',
'test/reporters',
'vfs',
]);
// Modules that will only be enabled at run time.
const experimentalModuleList = new SafeSet(['dtls', 'ffi', 'sqlite', 'quic', 'stream/iter', 'zlib/iter']);
const experimentalModuleList = new SafeSet([
'dtls',
'ffi',
'quic',
'sqlite',
'stream/iter',
'vfs',
'zlib/iter',
]);

// Set up process.binding() and process._linkedBinding().
{
Expand Down
Loading
Loading