Skip to main content

Overview

The VFS (Virtual File System) class provides a complete in-memory Unix-like filesystem with support for mounting external providers, content chunking for large files, and filesystem watching.

Creating a VFS

import { VFS } from '@lifo-sh/core';

const vfs = new VFS();

Constructor

constructor(contentStore?: ContentStore)
contentStore
ContentStore
Content store for chunked large files. Defaults to new ContentStore().

File Operations

readFile()

Read file contents as binary data.
readFile(path: string): Uint8Array
path
string
required
File path to read
data
Uint8Array
File contents as binary data
Example:
const vfs = new VFS();
vfs.writeFile('/test.txt', 'Hello');
const data = vfs.readFile('/test.txt');
console.log(data); // Uint8Array [72, 101, 108, 108, 111]
Throws: VFSError with code:
  • ENOENT - File or directory does not exist
  • EISDIR - Path is a directory, not a file

readFileString()

Read file contents as a UTF-8 string.
readFileString(path: string): string
path
string
required
File path to read
content
string
File contents as UTF-8 string
Example:
vfs.writeFile('/config.json', '{"key": "value"}');
const content = vfs.readFileString('/config.json');
console.log(JSON.parse(content)); // { key: 'value' }

writeFile()

Write file contents. Creates or overwrites the file.
writeFile(path: string, content: string | Uint8Array): void
path
string
required
File path to write
content
string | Uint8Array
required
File content (string or binary data)
Example:
// Write text
vfs.writeFile('/hello.txt', 'Hello, World!');

// Write binary
const binary = new Uint8Array([0x89, 0x50, 0x4E, 0x47]);
vfs.writeFile('/image.png', binary);

// Overwrite existing file
vfs.writeFile('/hello.txt', 'New content');
Throws: VFSError with code:
  • ENOENT - Parent directory does not exist
  • EISDIR - Path is a directory
  • EINVAL - Path is on a read-only mounted filesystem
Note: Files >= 1MB are automatically chunked into the ContentStore.

appendFile()

Append content to a file. Creates the file if it doesn’t exist.
appendFile(path: string, content: string | Uint8Array): void
path
string
required
File path to append to
content
string | Uint8Array
required
Content to append
Example:
vfs.writeFile('/log.txt', 'Line 1\n');
vfs.appendFile('/log.txt', 'Line 2\n');
vfs.appendFile('/log.txt', 'Line 3\n');
console.log(vfs.readFileString('/log.txt'));
// Output:
// Line 1
// Line 2
// Line 3

exists()

Check if a path exists.
exists(path: string): boolean
path
string
required
Path to check
exists
boolean
True if the path exists (file or directory)
Example:
vfs.writeFile('/test.txt', 'data');
console.log(vfs.exists('/test.txt')); // true
console.log(vfs.exists('/missing.txt')); // false

stat()

Get file or directory metadata.
stat(path: string): Stat
path
string
required
Path to stat
stat
Stat
File or directory metadata
Example:
vfs.writeFile('/doc.pdf', 'PDF data');
const stat = vfs.stat('/doc.pdf');
console.log(stat.type);  // 'file'
console.log(stat.size);  // 8
console.log(stat.mime);  // 'application/pdf'
console.log(new Date(stat.mtime)); // 2026-03-01T...
Throws: VFSError with code ENOENT if path does not exist. Delete a file.
unlink(path: string): void
path
string
required
File path to delete
Example:
vfs.writeFile('/temp.txt', 'data');
vfs.unlink('/temp.txt');
console.log(vfs.exists('/temp.txt')); // false
Throws: VFSError with code:
  • ENOENT - File does not exist
  • EISDIR - Path is a directory (use rmdir instead)
  • EINVAL - Path is on a read-only filesystem

rename()

Rename or move a file or directory.
rename(oldPath: string, newPath: string): void
oldPath
string
required
Current path
newPath
string
required
New path
Example:
vfs.writeFile('/old.txt', 'data');
vfs.rename('/old.txt', '/new.txt');
console.log(vfs.exists('/old.txt')); // false
console.log(vfs.exists('/new.txt')); // true

// Move between directories
vfs.mkdir('/src');
vfs.mkdir('/dst');
vfs.writeFile('/src/file.txt', 'data');
vfs.rename('/src/file.txt', '/dst/file.txt');
Throws: VFSError with code:
  • ENOENT - Source path does not exist
  • EINVAL - Cannot rename across mount boundaries

copyFile()

Copy a file.
copyFile(src: string, dest: string): void
src
string
required
Source file path
dest
string
required
Destination file path
Example:
vfs.writeFile('/original.txt', 'data');
vfs.copyFile('/original.txt', '/copy.txt');
console.log(vfs.readFileString('/copy.txt')); // 'data'
Throws: VFSError with code:
  • ENOENT - Source file does not exist
  • EISDIR - Source is a directory

touch()

Create an empty file or update the modification time of an existing file.
touch(path: string): void
path
string
required
File path to touch
Example:
vfs.touch('/new.txt');
console.log(vfs.exists('/new.txt')); // true
console.log(vfs.readFileString('/new.txt')); // ''

vfs.writeFile('/existing.txt', 'data');
const before = vfs.stat('/existing.txt').mtime;
vfs.touch('/existing.txt');
const after = vfs.stat('/existing.txt').mtime;
console.log(after > before); // true

Directory Operations

mkdir()

Create a directory.
mkdir(path: string, options?: { recursive?: boolean }): void
path
string
required
Directory path to create
options
object
Example:
// Simple mkdir
vfs.mkdir('/test');

// Recursive mkdir
vfs.mkdir('/a/b/c/d', { recursive: true });
console.log(vfs.exists('/a/b/c/d')); // true
Throws: VFSError with code:
  • ENOENT - Parent directory does not exist (without recursive)
  • EEXIST - Directory already exists
  • ENOTDIR - Parent path is not a directory
  • EINVAL - Path is on a read-only filesystem

rmdir()

Remove an empty directory.
rmdir(path: string): void
path
string
required
Directory path to remove
Example:
vfs.mkdir('/empty');
vfs.rmdir('/empty');
console.log(vfs.exists('/empty')); // false
Throws: VFSError with code:
  • ENOENT - Directory does not exist
  • ENOTDIR - Path is not a directory
  • ENOTEMPTY - Directory is not empty
  • EINVAL - Path is on a read-only filesystem

rmdirRecursive()

Recursively remove a directory and all its contents.
rmdirRecursive(path: string): void
path
string
required
Directory path to remove recursively
Example:
vfs.mkdir('/project/src', { recursive: true });
vfs.writeFile('/project/src/index.js', 'code');
vfs.writeFile('/project/README.md', 'docs');

vfs.rmdirRecursive('/project');
console.log(vfs.exists('/project')); // false
Throws: VFSError with code:
  • ENOENT - Directory does not exist
  • ENOTDIR - Path is not a directory

readdir()

List directory contents.
readdir(path: string): Dirent[]
path
string
required
Directory path to list
entries
Dirent[]
Array of directory entries
Example:
vfs.mkdir('/dir');
vfs.writeFile('/dir/a.txt', 'A');
vfs.writeFile('/dir/b.txt', 'B');
vfs.mkdir('/dir/subdir');

const entries = vfs.readdir('/dir');
for (const entry of entries) {
  console.log(entry.name, entry.type);
}
// Output:
// a.txt file
// b.txt file
// subdir directory
Throws: VFSError with code:
  • ENOENT - Directory does not exist
  • ENOTDIR - Path is not a directory

readdirStat()

List directory contents with full stat information.
readdirStat(path: string): Array<Dirent & Stat>
path
string
required
Directory path to list
entries
Array<Dirent & Stat>
Array of directory entries with stat info
Example:
vfs.mkdir('/dir');
vfs.writeFile('/dir/small.txt', 'Hi');
vfs.writeFile('/dir/large.txt', 'x'.repeat(1000));

const entries = vfs.readdirStat('/dir');
for (const entry of entries) {
  console.log(`${entry.name}: ${entry.size} bytes, ${entry.type}`);
}
// Output:
// small.txt: 2 bytes, file
// large.txt: 1000 bytes, file

Mount System

mount()

Mount a provider at an arbitrary path.
mount(path: string, provider: VirtualProvider | MountProvider): void
path
string
required
Virtual path where the provider will be mounted (e.g., “/proc”, “/mnt/project”)
provider
VirtualProvider | MountProvider
required
Provider implementing the VirtualProvider or MountProvider interface
Example:
import { VFS, NativeFsProvider } from '@lifo-sh/core';
import fs from 'node:fs';

const vfs = new VFS();

// Mount a native filesystem directory (Node.js only)
const provider = new NativeFsProvider('/home/user/project', fs);
vfs.mount('/mnt/project', provider);

// Now VFS operations on /mnt/project/* delegate to the native filesystem
const files = vfs.readdir('/mnt/project');

unmount()

Unmount a previously mounted provider.
unmount(path: string): void
path
string
required
Virtual path that was passed to mount()
Example:
vfs.unmount('/mnt/project');
Throws: VFSError with code EINVAL if path is not mounted.

registerProvider()

Backward-compatible alias for mount().
registerProvider(prefix: string, provider: VirtualProvider): void

Filesystem Watching

watch()

Watch for filesystem changes.
// Global watch (all changes)
watch(listener: VFSWatchListener): () => void

// Scoped watch (changes under a specific path)
watch(path: string, listener: VFSWatchListener): () => void
path
string
Path to watch (if omitted, watches all changes)
listener
VFSWatchListener
required
Callback function invoked on filesystem changes
unwatch
() => void
Function to stop watching
Example:
// Watch all changes
const unwatch = vfs.watch((event) => {
  console.log(event.type, event.path);
});

vfs.writeFile('/test.txt', 'data'); // Logs: create /test.txt
vfs.writeFile('/test.txt', 'new');  // Logs: modify /test.txt
vfs.unlink('/test.txt');            // Logs: delete /test.txt

unwatch(); // Stop watching

// Watch specific directory
const unwatchHome = vfs.watch('/home/user', (event) => {
  console.log('Home changed:', event.type, event.path);
});

vfs.writeFile('/home/user/file.txt', 'data'); // Triggers callback
vfs.writeFile('/tmp/other.txt', 'data');      // Does not trigger
Event types:
interface VFSWatchEvent {
  type: 'create' | 'modify' | 'delete' | 'rename';
  path: string;           // Affected path
  oldPath?: string;       // Previous path (rename events only)
  fileType: 'file' | 'directory';
}

Serialization

getRoot()

Get the root INode for serialization.
getRoot(): INode
root
INode
Root filesystem node
Example:
const root = vfs.getRoot();
const serialized = JSON.stringify(root);

loadFromSerialized()

Load filesystem from a serialized root node.
loadFromSerialized(root: INode): void
root
INode
required
Root node to restore
Example:
const root = JSON.parse(serialized);
const vfs = new VFS();
vfs.loadFromSerialized(root);

Type Definitions

interface Stat {
  type: 'file' | 'directory';
  size: number;
  ctime: number;  // Creation time (ms since epoch)
  mtime: number;  // Modification time (ms since epoch)
  mode: number;   // Unix file mode (e.g., 0o644)
  mime?: string;  // MIME type (files only)
}

interface Dirent {
  name: string;
  type: 'file' | 'directory';
}

interface VFSWatchEvent {
  type: 'create' | 'modify' | 'delete' | 'rename';
  path: string;
  oldPath?: string;  // Only for 'rename' events
  fileType: 'file' | 'directory';
}

type VFSWatchListener = (event: VFSWatchEvent) => void;

class VFSError extends Error {
  code: 'ENOENT' | 'EEXIST' | 'ENOTDIR' | 'EISDIR' | 'ENOTEMPTY' | 'EINVAL';
}

Content Chunking

Files >= 1MB are automatically chunked into the ContentStore:
  • Small files (< 1MB): Stored inline in the INode
  • Large files (>= 1MB): Split into 256KB chunks, stored in ContentStore
This optimization reduces memory usage for large files while keeping small files fast. Example:
const vfs = new VFS();

// Small file - stored inline
vfs.writeFile('/small.txt', 'x'.repeat(100_000)); // 100 KB

// Large file - automatically chunked
vfs.writeFile('/large.txt', 'x'.repeat(2_000_000)); // 2 MB

const stat = vfs.stat('/large.txt');
console.log(stat.size); // 2000000 (size is still accurate)

// Reading works transparently
const content = vfs.readFileString('/large.txt');
console.log(content.length); // 2000000

Source Location

// src/kernel/vfs/VFS.ts (604 lines)
export class VFS {
  constructor(contentStore?: ContentStore);
  
  // File operations
  readFile(path: string): Uint8Array;
  readFileString(path: string): string;
  writeFile(path: string, content: string | Uint8Array): void;
  appendFile(path: string, content: string | Uint8Array): void;
  exists(path: string): boolean;
  stat(path: string): Stat;
  unlink(path: string): void;
  rename(oldPath: string, newPath: string): void;
  copyFile(src: string, dest: string): void;
  touch(path: string): void;
  
  // Directory operations
  mkdir(path: string, options?: { recursive?: boolean }): void;
  rmdir(path: string): void;
  rmdirRecursive(path: string): void;
  readdir(path: string): Dirent[];
  readdirStat(path: string): Array<Dirent & Stat>;
  
  // Mount system
  mount(path: string, provider: VirtualProvider | MountProvider): void;
  unmount(path: string): void;
  registerProvider(prefix: string, provider: VirtualProvider): void;
  
  // Watching
  watch(listener: VFSWatchListener): () => void;
  watch(path: string, listener: VFSWatchListener): () => void;
  
  // Serialization
  getRoot(): INode;
  loadFromSerialized(root: INode): void;
}

Build docs developers (and LLMs) love