memfs implements Node's fs API in memory:
synchronous, callback, and promise-based methods; read/write streams; file and
directory watching; hard links and symlinks; i-nodes; file descriptors; and the
fs.constants. It throws same errors as Node (with .code set, e.g.
ENOENT), so code that branches on error codes keeps working.
import { fs } from 'memfs';
fs.writeFileSync('/hello.txt', 'World!');
fs.readFileSync('/hello.txt', 'utf8'); // 'World!'
memfs() --- isolated instancesfs/vol are a single shared default volume. For tests, prefer memfs(),
which returns a brand-new, isolated pair so volumes never leak into each other:
import { memfs } from 'memfs';
const { fs, vol } = memfs();
Seed it from a nested JSON tree:
const { fs } = memfs({
'/app': {
'index.js': 'console.log(1)',
'package.json': '{"name": "app"}',
},
});
fs.readdirSync('/app'); // ['index.js', 'package.json']
The second argument is a cwd string or an options object:
interface MemfsOptions {
/** Working directory for resolving relative paths. Defaults to '/'. */
cwd?: string;
/** A process-like object controlling platform, uid, gid, and cwd(). */
process?: IProcess;
}
memfs(json?: NestedDirectoryJSON, cwdOrOpts?: string | MemfsOptions): {fs: IFs; vol: Volume};
const { fs } = memfs({ './README.md': '# Hi' }, '/repo');
fs.readFileSync('/repo/README.md', 'utf8'); // '# Hi'
fs vs volThe package exports both fs and vol. They back onto the same storage but
differ in shape:
import { fs, vol } from 'memfs';
vol is a Volume instance --- it implements every fs method, plus
volume helpers like fromJSON/toJSON/reset/toTree. Its methods are
not bound and it carries no constants:
vol.writeFileSync('/foo', 'bar');
vol.F_OK; // undefined
fs is an fs-like object built from vol with createFsFromVolume(vol).
All methods are bound (safe to destructure) and
it carries constants, Stats, Dirent, ReadStream, promises, etc. ---
identical in shape to require('fs'):
const { readFileSync, writeFileSync } = fs; // bound, safe to destructure
fs.constants.O_RDONLY; // 0
Every member of the fs object is also re-exported at the top level, so you can
treat memfs itself as the fs module:
import { readFileSync, F_OK, ReadStream } from 'memfs';
Use vol when you want the volume helpers; use fs (or memfs()) when you want
a faithful fs drop-in.
Volume and createFsFromVolumememfs() is sugar over two lower-level pieces you can use directly:
import { Volume, createFsFromVolume } from 'memfs';
const vol = new Volume();
const fs = createFsFromVolume(vol);
fs.writeFileSync('/foo', 'bar');
new Volume() is an empty filesystem. createFsFromVolume(vol) wraps it into
the bound, constants-carrying fs-like object described above. Construct as
many independent volumes as you need --- see Volumes.
fs.promises (equivalently vol.promises) mirrors fs/promises:
const { fs } = memfs();
await fs.promises.writeFile('/note.txt', 'hi');
await fs.promises.readFile('/note.txt', 'utf8'); // 'hi'
const handle = await fs.promises.open('/note.txt', 'r');
const { bytesRead, buffer } = await handle.read(Buffer.alloc(2), 0, 2, 0);
await handle.close();
createReadStream and createWriteStream return Node-compatible streams:
const { fs } = memfs({ '/in.txt': 'stream me' });
const out = fs.createWriteStream('/out.txt');
fs.createReadStream('/in.txt').pipe(out);
out.on('finish', () => fs.readFileSync('/out.txt', 'utf8')); // 'stream me'
Both fs.watch (inode events) and fs.watchFile (stat polling) are
implemented:
const { fs } = memfs({ '/log.txt': '' });
const watcher = fs.watch('/log.txt', (eventType, filename) => {
// eventType: 'change' | 'rename'
});
fs.appendFileSync('/log.txt', 'entry\n');
watcher.close();
These are available both as named exports and as properties of the fs object.
| Object | Description |
|---|---|
Stats |
Result of stat/lstat/fstat. Stats<bigint> when {bigint: true} |
Dirent |
Directory entry from readdir({withFileTypes: true}) and Dir. Exposes name, parentPath, and isFile()/isDirectory()/isSymbolicLink()/... (the legacy path getter is deprecated --- use parentPath) |
Dir |
Async directory iterator from opendir. Implements AsyncIterable<Dirent> and Symbol.asyncDispose, so it works with await using |
StatFs |
Result of statfs/statfsSync |
FileHandle |
Returned by fs.promises.open() |
StatWatcher |
Returned by watchFile |
FSWatcher |
Returned by watch |
Dir with await using (auto-closes on scope exit):
const { fs } = memfs({ '/d': { a: '', b: '' } });
await using dir = await fs.promises.opendir('/d');
for await (const entry of dir) {
entry.name; // 'a' then 'b'
entry.parentPath; // '/d'
entry.isFile(); // true
}
Absolute paths behave as you would expect. Relative paths are resolved
against process.cwd() --- which points at your on-disk working directory,
a folder that almost certainly does not exist inside the in-memory volume. The
safe choice is to always use absolute paths.
If you must use relative paths, either create the cwd inside the volume:
vol.mkdirSync(process.cwd(), { recursive: true });
or point the process at /, which exists in every volume:
process.chdir('/');
(You can also pass a cwd to memfs() or a custom
process object to control this per volume.)
The Node fs implementation relies on the buffer, events, stream, and
path built-ins, and uses the process and setImmediate globals (mocking
them when unavailable), so it bundles cleanly for the browser.