Skip to content

Commit c9562dd

Browse files
authored
vfs: add minimal node:vfs subsystem
Adds the node:vfs builtin module with VirtualFileSystem and provider classes. No integration with fs, modules, or SEA. Assisted-by: Claude-Opus4.7 Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: nodejs#63115 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent c9dbb86 commit c9562dd

79 files changed

Lines changed: 10859 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

doc/api/cli.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,16 @@ The flag may be specified more than once; tests must contain **every**
14441444
filter value to run. See [Test tags][] for details on declaring and
14451445
inheriting tags.
14461446

1447+
### `--experimental-vfs`
1448+
1449+
<!-- YAML
1450+
added: REPLACEME
1451+
-->
1452+
1453+
> Stability: 1 - Experimental
1454+
1455+
Enable the experimental [`node:vfs`][] module.
1456+
14471457
### `--experimental-vm-modules`
14481458

14491459
<!-- YAML
@@ -3786,6 +3796,7 @@ one is included in the list below.
37863796
* `--experimental-stream-iter`
37873797
* `--experimental-test-isolation`
37883798
* `--experimental-top-level-await`
3799+
* `--experimental-vfs`
37893800
* `--experimental-vm-modules`
37903801
* `--experimental-wasi-unstable-preview1`
37913802
* `--force-context-aware`
@@ -4428,6 +4439,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
44284439
[`node:ffi`]: ffi.md
44294440
[`node:sqlite`]: sqlite.md
44304441
[`node:stream/iter`]: stream_iter.md
4442+
[`node:vfs`]: vfs.md
44314443
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
44324444
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
44334445
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version

doc/api/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
* [URL](url.md)
6666
* [Utilities](util.md)
6767
* [V8](v8.md)
68+
* [Virtual File System](vfs.md)
6869
* [VM](vm.md)
6970
* [WASI](wasi.md)
7071
* [Web Crypto API](webcrypto.md)

doc/api/vfs.md

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Virtual File System
2+
3+
<!--introduced_in=REPLACEME-->
4+
5+
<!-- YAML
6+
added: REPLACEME
7+
-->
8+
9+
> Stability: 1 - Experimental
10+
11+
<!-- source_link=lib/vfs.js -->
12+
13+
The `node:vfs` module provides an in-memory virtual file system with a
14+
`node:fs`-like API. It is useful for tests, fixtures, embedded assets, and other
15+
scenarios where you need a self-contained file system without touching the
16+
actual file-system.
17+
18+
To access it:
19+
20+
```mjs
21+
import vfs from 'node:vfs';
22+
```
23+
24+
```cjs
25+
const vfs = require('node:vfs');
26+
```
27+
28+
This module is only available under the `node:` scheme, and only when Node.js
29+
is started with the `--experimental-vfs` flag.
30+
31+
## Basic usage
32+
33+
```cjs
34+
const vfs = require('node:vfs');
35+
36+
const myVfs = vfs.create();
37+
myVfs.mkdirSync('/dir', { recursive: true });
38+
myVfs.writeFileSync('/dir/hello.txt', 'Hello, VFS!');
39+
40+
console.log(myVfs.readFileSync('/dir/hello.txt', 'utf8')); // 'Hello, VFS!'
41+
```
42+
43+
`vfs.create()` returns a [`VirtualFileSystem`][] instance backed by a
44+
[`MemoryProvider`][] by default. The instance exposes synchronous,
45+
callback-based, and promise-based file system methods that mirror the
46+
shape of the [`node:fs`][] API. All paths are POSIX-style and absolute
47+
(starting with `/`).
48+
49+
## `vfs.create([provider][, options])`
50+
51+
<!-- YAML
52+
added: REPLACEME
53+
-->
54+
55+
* `provider` {VirtualProvider} The provider to use. **Default:**
56+
`new MemoryProvider()`.
57+
* `options` {Object}
58+
* `emitExperimentalWarning` {boolean} Whether to emit the experimental
59+
warning when the instance is created. **Default:** `true`.
60+
* Returns: {VirtualFileSystem}
61+
62+
Convenience factory equivalent to `new VirtualFileSystem(provider, options)`.
63+
64+
```cjs
65+
const vfs = require('node:vfs');
66+
67+
// Default in-memory provider
68+
const memoryVfs = vfs.create();
69+
70+
// Explicit provider
71+
const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox'));
72+
```
73+
74+
## Class: `VirtualFileSystem`
75+
76+
<!-- YAML
77+
added: REPLACEME
78+
-->
79+
80+
A `VirtualFileSystem` wraps a [`VirtualProvider`][] and exposes a
81+
`node:fs`-like API. Each instance maintains its own file tree.
82+
83+
### `new VirtualFileSystem([provider][, options])`
84+
85+
<!-- YAML
86+
added: REPLACEME
87+
-->
88+
89+
* `provider` {VirtualProvider} The provider to use. **Default:**
90+
`new MemoryProvider()`.
91+
* `options` {Object}
92+
* `emitExperimentalWarning` {boolean} Whether to emit the experimental
93+
warning. **Default:** `true`.
94+
95+
### `vfs.provider`
96+
97+
<!-- YAML
98+
added: REPLACEME
99+
-->
100+
101+
* {VirtualProvider}
102+
103+
The provider backing this VFS instance.
104+
105+
### `vfs.readonly`
106+
107+
<!-- YAML
108+
added: REPLACEME
109+
-->
110+
111+
* {boolean}
112+
113+
`true` when the underlying provider is read-only.
114+
115+
### APIs
116+
117+
`VirtualFileSystem` implements the following methods, with the same
118+
signatures as their [`node:fs`][] counterparts:
119+
120+
#### Synchronous API
121+
122+
* `existsSync(path)`
123+
* `statSync(path[, options])`
124+
* `lstatSync(path[, options])`
125+
* `readFileSync(path[, options])`
126+
* `writeFileSync(path, data[, options])`
127+
* `appendFileSync(path, data[, options])`
128+
* `readdirSync(path[, options])`
129+
* `mkdirSync(path[, options])`
130+
* `rmdirSync(path)`
131+
* `unlinkSync(path)`
132+
* `renameSync(oldPath, newPath)`
133+
* `copyFileSync(src, dest[, mode])`
134+
* `realpathSync(path[, options])`
135+
* `readlinkSync(path[, options])`
136+
* `symlinkSync(target, path[, type])`
137+
* `accessSync(path[, mode])`
138+
* `rmSync(path[, options])`
139+
* `truncateSync(path[, len])`
140+
* `ftruncateSync(fd[, len])`
141+
* `linkSync(existingPath, newPath)`
142+
* `chmodSync(path, mode)`
143+
* `chownSync(path, uid, gid)`
144+
* `utimesSync(path, atime, mtime)`
145+
* `lutimesSync(path, atime, mtime)`
146+
* `mkdtempSync(prefix)`
147+
* `opendirSync(path[, options])`
148+
* `openAsBlob(path[, options])`
149+
* File-descriptor ops: `openSync`, `closeSync`, `readSync`, `writeSync`,
150+
`fstatSync`
151+
* Streams: `createReadStream`, `createWriteStream`
152+
* Watchers: `watch`, `watchFile`, `unwatchFile`
153+
154+
#### Callback API
155+
156+
`readFile`, `writeFile`, `stat`, `lstat`, `readdir`, `realpath`, `readlink`,
157+
`access`, `open`, `close`, `read`, `write`, `rm`, `fstat`, `truncate`,
158+
`ftruncate`, `link`, `mkdtemp`, `opendir`. Each takes a Node.js-style
159+
callback `(err, ...result) => {}`.
160+
161+
#### Promise API
162+
163+
`vfs.promises` exposes the promise-based variants:
164+
165+
```cjs
166+
const vfs = require('node:vfs');
167+
168+
async function example() {
169+
const myVfs = vfs.create();
170+
await myVfs.promises.writeFile('/file.txt', 'hello');
171+
const data = await myVfs.promises.readFile('/file.txt', 'utf8');
172+
return data;
173+
}
174+
example();
175+
```
176+
177+
The promise namespace mirrors `fs.promises` and includes `readFile`,
178+
`writeFile`, `appendFile`, `stat`, `lstat`, `readdir`, `mkdir`, `rmdir`,
179+
`unlink`, `rename`, `copyFile`, `realpath`, `readlink`, `symlink`,
180+
`access`, `rm`, `truncate`, `link`, `mkdtemp`, `chmod`, `chown`, `lchown`,
181+
`utimes`, `lutimes`, `open`, `lchmod`, and `watch`.
182+
183+
## Class: `VirtualProvider`
184+
185+
<!-- YAML
186+
added: REPLACEME
187+
-->
188+
189+
The base class for all VFS providers. Subclasses implement the essential
190+
primitives (`open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`,
191+
`rename`, ...) and inherit default implementations of the derived
192+
The base class for all VFS providers. Subclasses implement the essential
193+
primitives (such as `open`, `stat`, `readdir`, `mkdir`, `rmdir`, `unlink`,
194+
`rename`, etc.) and inherit default implementations of the derived
195+
methods (such as `readFile`, `writeFile`, `exists`, `copyFile`, `access`, etc.).
196+
197+
### Capability flags
198+
199+
* `provider.readonly` {boolean} **Default:** `false`.
200+
* `provider.supportsSymlinks` {boolean} **Default:** `false`.
201+
* `provider.supportsWatch` {boolean} **Default:** `false`.
202+
203+
### Creating custom providers
204+
205+
```cjs
206+
const { VirtualProvider } = require('node:vfs');
207+
208+
class StaticProvider extends VirtualProvider {
209+
get readonly() { return true; }
210+
211+
statSync(path) { /* ... */ }
212+
openSync(path, flags) { /* ... */ }
213+
readdirSync(path, options) { /* ... */ }
214+
// ...
215+
}
216+
```
217+
218+
The base class throws `ERR_METHOD_NOT_IMPLEMENTED` for any primitive
219+
that has not been overridden, and rejects writes from a `readonly`
220+
provider with `EROFS`.
221+
222+
## Class: `MemoryProvider`
223+
224+
<!-- YAML
225+
added: REPLACEME
226+
-->
227+
228+
The default in-memory provider. Stores files, directories, and symbolic
229+
links in a `Map`-backed tree, supports symlinks (`supportsSymlinks ===
230+
true`), and supports watching (`supportsWatch === true`).
231+
232+
### `memoryProvider.setReadOnly()`
233+
234+
<!-- YAML
235+
added: REPLACEME
236+
-->
237+
238+
Locks the provider into read-only mode. Subsequent writes through any
239+
[`VirtualFileSystem`][] using this provider throw `EROFS`. There is no
240+
way to revert the provider to writable.
241+
242+
```cjs
243+
const vfs = require('node:vfs');
244+
245+
const provider = new vfs.MemoryProvider();
246+
const myVfs = vfs.create(provider);
247+
myVfs.writeFileSync('/seed.txt', 'initial');
248+
249+
provider.setReadOnly();
250+
251+
myVfs.writeFileSync('/x.txt', 'fail'); // throws EROFS
252+
```
253+
254+
## Class: `RealFSProvider`
255+
256+
<!-- YAML
257+
added: REPLACEME
258+
-->
259+
260+
A provider that wraps a directory (i.e. one on the actual file system) and exposes its
261+
contents through the VFS API. All VFS paths are resolved relative to
262+
the root and verified to stay inside it; symbolic links resolving
263+
outside the root are rejected.
264+
265+
### `new RealFSProvider(rootPath)`
266+
267+
<!-- YAML
268+
added: REPLACEME
269+
-->
270+
271+
* `rootPath` {string} The absolute file-system path to use as the root.
272+
Must be a non-empty string.
273+
274+
```cjs
275+
const vfs = require('node:vfs');
276+
277+
const realVfs = vfs.create(new vfs.RealFSProvider('/tmp/sandbox'));
278+
realVfs.writeFileSync('/file.txt', 'hello'); // writes /tmp/sandbox/file.txt
279+
```
280+
281+
### `realFSProvider.rootPath`
282+
283+
<!-- YAML
284+
added: REPLACEME
285+
-->
286+
287+
* {string}
288+
289+
The resolved absolute path used as the root.
290+
291+
## Implementation details
292+
293+
### `Stats` objects
294+
295+
VFS `Stats` objects are real instances of [`fs.Stats`][] (or
296+
[`fs.BigIntStats`][] when `{ bigint: true }` is requested). Their
297+
fields use synthetic but stable values:
298+
299+
* `dev` is `4085` (the VFS device id).
300+
* `ino` is monotonically increasing per process.
301+
* `blksize` is `4096`.
302+
* `blocks` is `Math.ceil(size / 512)`.
303+
* Times default to the moment the entry was created/last modified.
304+
305+
[`MemoryProvider`]: #class-memoryprovider
306+
[`VirtualFileSystem`]: #class-virtualfilesystem
307+
[`VirtualProvider`]: #class-virtualprovider
308+
[`fs.BigIntStats`]: fs.md#class-fsbigintstats
309+
[`fs.Stats`]: fs.md#class-fsstats
310+
[`node:fs`]: fs.md

doc/node.1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,11 @@ Enable the experimental
758758
.Sy node:stream/iter
759759
module.
760760
.
761+
.It Fl -experimental-vfs
762+
Enable the experimental
763+
.Sy node:vfs
764+
module.
765+
.
761766
.It Fl -experimental-sea-config
762767
Use this flag to generate a blob that can be injected into the Node.js
763768
binary to produce a single executable application. See the documentation
@@ -1945,6 +1950,8 @@ one is included in the list below.
19451950
.It
19461951
\fB--experimental-top-level-await\fR
19471952
.It
1953+
\fB--experimental-vfs\fR
1954+
.It
19481955
\fB--experimental-vm-modules\fR
19491956
.It
19501957
\fB--experimental-wasi-unstable-preview1\fR

lib/internal/bootstrap/realm.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,18 @@ const schemelessBlockList = new SafeSet([
131131
'quic',
132132
'test',
133133
'test/reporters',
134+
'vfs',
134135
]);
135136
// Modules that will only be enabled at run time.
136-
const experimentalModuleList = new SafeSet(['dtls', 'ffi', 'sqlite', 'quic', 'stream/iter', 'zlib/iter']);
137+
const experimentalModuleList = new SafeSet([
138+
'dtls',
139+
'ffi',
140+
'quic',
141+
'sqlite',
142+
'stream/iter',
143+
'vfs',
144+
'zlib/iter',
145+
]);
137146

138147
// Set up process.binding() and process._linkedBinding().
139148
{

0 commit comments

Comments
 (0)