volumes.md 4.4 KB

A Volume is one isolated in-memory filesystem. You can spin up as many as you like, seed them from a plain JSON object, and export their contents back to JSON --- which makes memfs convenient for fixtures and assertions.

import { Volume } from 'memfs';

const vol = Volume.fromJSON({ '/foo': 'bar' });
vol.readFileSync('/foo', 'utf8'); // 'bar'

Building from JSON

There are two JSON shapes. Flat maps full paths to contents:

type DirectoryJSON = { [path: string]: string | Buffer | null };
const vol = Volume.fromJSON(
  {
    './README.md': '1',
    './src/index.js': '2',
    './node_modules/debug/index.js': '3',
  },
  '/app', // cwd: resolves the relative keys above
);

vol.readFileSync('/app/README.md', 'utf8'); // '1'
vol.readFileSync('/app/src/index.js', 'utf8'); // '2'

Nested lets directories nest as objects --- handy for deeper trees:

const vol = Volume.fromNestedJSON({
  '/app': {
    'index.js': '...',
    src: {
      'main.ts': '...',
      util: { 'log.ts': '...' },
    },
  },
});

In both shapes the value null means an empty directory and an empty string '' means an empty file. Values can be string or Buffer (binary). The instance methods vol.fromJSON(json, cwd?) and vol.fromNestedJSON(json, cwd?) add files into an existing volume; the static Volume.fromJSON / Volume.fromNestedJSON create a new one.

const vol = new Volume();
vol.fromJSON({ '/a.txt': 'A' });
vol.fromJSON({ '/b.txt': 'B' }); // merges in
`vol.mountSync(mountpoint, json)` is a legacy alias for adding a flat JSON tree
rooted at a given path. New code should use `fromJSON` with a cwd.

Exporting to JSON

toJSON walks the volume and returns a flat object --- the inverse of fromJSON. This is the workhorse for test assertions:

vol.writeFileSync('/foo', 'bar');
vol.toJSON(); // {'/foo': 'bar'}
expect(vol.toJSON()).toEqual({ '/foo': 'bar' });

The full signature lets you scope and shape the output:

vol.toJSON(
  paths?: PathLike | PathLike[], // restrict to these paths; omit for everything
  json?: {},                     // object to populate (for merging exports)
  isRelative?: boolean,          // emit relative instead of absolute paths
  asBuffer?: boolean,            // emit Buffer contents instead of strings
): DirectoryJSON;
const vol = Volume.fromJSON({ '/dir/a': 'b', '/dir2/a': 'b', '/dir2/c': 'd' });
vol.toJSON('/dir2'); // {'/dir2/a': 'b', '/dir2/c': 'd'}

Reset

reset() empties a volume so you can reuse it between tests:

vol.fromJSON({ '/index.js': '...' });
vol.toJSON(); // {'/index.js': '...'}
vol.reset();
vol.toJSON(); // {}

Inspecting as a tree

toTree() renders the volume as an ASCII tree (a quick built-in; for arbitrary fs filesystems and more options see Tree printing):

const { vol } = memfs({
  '/src': {
    'index.ts': '...',
    util: { 'print.ts': '...' },
  },
});

console.log(vol.toTree());
// /
// └─ src/
//    ├─ index.ts
//    └─ util/
//       └─ print.ts

toTree(opts?) accepts a sub-folder, a depth, and a separator.

Many volumes

Each Volume is fully independent:

const a = Volume.fromJSON({ '/foo': 'bar' });
const b = Volume.fromJSON({ '/foo': 'baz' });

a.readFileSync('/foo', 'utf8'); // 'bar'
b.readFileSync('/foo', 'utf8'); // 'baz'

Reach for memfs() when you also want the bound, constants-carrying fs object alongside the volume --- see the Node fs API page.

Combine with the real disk: unionfs

unionfs layers several filesystems into one. Overlay an in-memory volume on top of the real disk:

import * as realFs from 'fs';
import { ufs } from 'unionfs';
import { Volume } from 'memfs';

const vol = Volume.fromJSON({ '/foo': 'bar' });

ufs.use(realFs).use(vol);
ufs.readFileSync('/foo', 'utf8'); // 'bar', served from the volume

Patch require: fs-monkey

fs-monkey can point Node's module loader at a volume, so require() resolves modules out of memory:

import { patchRequire } from 'fs-monkey';

vol.writeFileSync('/index.js', 'console.log("hi world")');
patchRequire(vol);
require('/index'); // logs: hi world

fs-monkey also exposes patchFs(vol) to monkey-patch the global fs module itself.