-
-
Notifications
You must be signed in to change notification settings - Fork 35.5k
vfs: add minimal node:vfs subsystem #63115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
adb398b
ce68078
78cbe8f
6f07670
f44f6fb
43c5876
2395931
d2c6d4e
2d78d3a
bb3beae
a55b8d8
6a9b3af
afb36cf
bf7f7f7
b703045
a487356
3709515
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,307 @@ | ||||||||||||||||||
| # 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 an | ||||||||||||||||||
| `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. | ||||||||||||||||||
|
|
||||||||||||||||||
| ### File system methods | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On fs.md, we call it "API", which I find more straight forward
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| `VirtualFileSystem` implements the following methods, with the same | ||||||||||||||||||
| signatures as their [`node:fs`][] counterparts: | ||||||||||||||||||
|
|
||||||||||||||||||
| #### Synchronous methods | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| * `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-style asynchronous methods | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| `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)`. | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without the arrow, I find it a bit confusing
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| #### Promise methods | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| `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 | ||||||||||||||||||
| methods (`readFile`, `writeFile`, `exists`, `copyFile`, `access`, ...). | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to https://learn.microsoft.com/en-us/style-guide/punctuation/ellipses, we should not be using an ellipsis here
Suggested change
( Line 97 in a159b57
Alternatively, we can list them all, or not at all
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| ### 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 real file system directory and exposes its | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
| 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. | ||||||||||||||||||
|
Comment on lines
+268
to
+269
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We should probably document what happens if |
||||||||||||||||||
|
|
||||||||||||||||||
| ```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 | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.