@studnicky/virtual-fs
In-memory synchronous filesystem primitive. Gives file-lock (and any other fs-dependent code) a browser-compatible backend. Subclass to observe every filesystem event.
Install
pnpm add @studnicky/virtual-fsRequires @studnicky:registry=https://npm.pkg.github.com in .npmrc.
@studnicky/virtual-fs declares a root usage API and explicit public subpaths.
The root VirtualFileSystem remains the synchronous in-memory primitive. For durable async files, @studnicky/virtual-fs/node exposes Node promise-based files and @studnicky/virtual-fs/browser exposes native Origin Private File System storage.
Usage
Create an instance with VirtualFileSystem.create(options?), seed files, then call the familiar synchronous methods:
import { VirtualFileSystem } from '../src/index.js';
const vfs = VirtualFileSystem.create({
'seed': new Map([['/data/hello.txt', 'Hello, virtual world!']])
});
// Ensure /data directory entry exists for readdirSync
vfs.mkdirSync('/data', { 'recursive': true });
// Write a second file
vfs.writeFileSync('/data/config.json', '{"version":1}', 'utf8');
// Read both files
const hello = vfs.readFileSync('/data/hello.txt', 'utf8');
console.log(`hello.txt: ${hello}`);
const config = vfs.readFileSync('/data/config.json', 'utf8');
console.log(`config.json: ${config}`);
// Rename config.json → settings.json
vfs.renameSync('/data/config.json', '/data/settings.json');
console.log('renamed config.json → settings.json');
// List directory
const entries = vfs.readdirSync('/data');
console.log(`/data entries: ${entries.join(', ')}`);
// Stat the renamed file
const stat = vfs.statSync('/data/settings.json');
console.log(`settings.json isFile=${String(stat.isFile())} isDirectory=${String(stat.isDirectory())} mtimeMs=${stat.mtimeMs}`);Try it
Factory demo
The factory seeds /data/hello.txt, writes a second file, renames it, reads the directory listing, and stats the renamed file. All assertions verify the expected state.
Lifecycle hooks
TracingVfs subclasses VirtualFileSystem and overrides all five hooks: onCreate, onWrite, onRead, onRename, and onDelete. The demo exercises every path — seeding (triggers onCreate), overwriting (triggers onWrite), reading, renaming, and unlinking — printing a full hook trace.
Origin Private File System
OpfsFileSystem implements the asynchronous durable-file contract through the browser Origin Private File System API.
Observability hooks
Subclass VirtualFileSystem and override any protected hook to inject trace logging, metrics, or side-effects at the exact stage where they are needed. Hooks should stay fast and non-blocking; observer-hook failures are contained so the filesystem operation still wins.
| Hook | When it fires | Args |
|---|---|---|
onCreate(path) | A new file or directory is created (writeFileSync on a new path, mkdirSync) | path: string |
onWrite(path) | An existing file is overwritten (writeFileSync on an existing path) | path: string |
onRead(path) | A file or directory is read (readFileSync, readdirSync) | path: string |
onRename(oldPath, newPath) | A file is renamed (renameSync) | oldPath: string, newPath: string |
onDelete(path) | A file is deleted (unlinkSync) | path: string |
import { EventRecorder } from '@studnicky/errors';
import assert from 'node:assert/strict';
import type { HookEventEntity } from './entities/HookEventEntity.js';
import { VirtualFileSystem } from '../src/index.js';
const recorder = new EventRecorder<HookEventEntity.Type>();
class TracingVfs extends VirtualFileSystem {
protected override onCreate(path: string): void {
recorder.record({ 'hook': 'onCreate', 'path': path }, `[virtual-fs] onCreate path=${path}`);
}
protected override onDelete(path: string): void {
recorder.record({ 'hook': 'onDelete', 'path': path }, `[virtual-fs] onDelete path=${path}`);
}
protected override onRead(path: string): void {
recorder.record({ 'hook': 'onRead', 'path': path }, `[virtual-fs] onRead path=${path}`);
}
protected override onRename(oldPath: string, newPath: string): void {
recorder.record({ 'hook': 'onRename', 'path': oldPath }, `[virtual-fs] onRename from=${oldPath} to=${newPath}`);
}
protected override onWrite(path: string): void {
recorder.record({ 'hook': 'onWrite', 'path': path }, `[virtual-fs] onWrite path=${path}`);
}
}
// Build without seeding — write the initial file after construction so hooks
// fire after class field initializers have run (events array is ready).
const vfs = TracingVfs.create();
// Write initial file → onCreate
vfs.writeFileSync('/log/init.txt', 'bootstrap', 'utf8');
console.log('--- initial write complete ---');
// Write to same file → onWrite
vfs.writeFileSync('/log/init.txt', 'updated', 'utf8');
// Write new file → onCreate
vfs.writeFileSync('/log/new.txt', 'brand new', 'utf8');
// Read → onRead
vfs.readFileSync('/log/init.txt', 'utf8');
// Mkdir + Readdir → onCreate (on /log/sub) then onRead (on /log)
vfs.mkdirSync('/log', { 'recursive': true });
vfs.mkdirSync('/log/sub', { 'recursive': true });
vfs.readdirSync('/log');
// Rename → onRename
vfs.renameSync('/log/new.txt', '/log/renamed.txt');
// Unlink → onDelete
vfs.unlinkSync('/log/renamed.txt');
console.log('--- hook trace complete ---');The base class never calls any logger or metrics library. All hooks are no-ops by default.
Injectable clock
Pass a @studnicky/clock ClockProviderInterface through VirtualFileSystem.create({ clock }) to control mtimeMs timestamps for deterministic test scenarios:
import type { ClockProviderInterface } from '@studnicky/clock';
import { VirtualFileSystem } from '@studnicky/virtual-fs';
// Any ClockProviderInterface drives mtimeMs — here a fixed, deterministic clock.
const clock: ClockProviderInterface = {
hrtime: () => 1_000_000_000n,
now: () => 1000
};
const vfs = VirtualFileSystem.create({ clock });FileSystemInterface contract
VirtualFileSystem implements FileSystemInterface, which is also exported from @studnicky/virtual-fs. Any code that depends on filesystem access can accept FileSystemInterface and receive either the real Node.js fs module adapter or a VirtualFileSystem — enabling browser-safe and test-isolated execution of the same logic.
import type { FileSystemInterface } from '@studnicky/virtual-fs';
function processFiles(fs: FileSystemInterface): void {
const entries = fs.readdirSync('/data');
// works in Node with NodeFileSystem or in the browser with VirtualFileSystem
}Async files
AsyncFileSystemInterface is the shared contract for durable asynchronous files. It supports existence checks, directory creation and listing, file reads and writes, and recursive removal. Use NodeFileSystem on the server or OpfsFileSystem in browsers that provide OPFS.
Public API
The root exports VirtualFileSystem, VirtualFileSystemError, and FileSystemInterface. Filesystem entities use @studnicky/virtual-fs/entities; option and stat contracts use @studnicky/virtual-fs/interfaces.
Entities
@studnicky/virtual-fs/entities exports every schema namespace in src/entities.
import { EntryEntity } from '@studnicky/virtual-fs/entities';Interfaces
@studnicky/virtual-fs/interfaces exports every TypeScript interface in src/interfaces, including configuration and state contracts.
import type { StatResultInterface } from '@studnicky/virtual-fs/interfaces';Exports
| Symbol | Purpose | Import path |
|---|---|---|
FileSystemInterface | Defines the synchronous in-memory file system contract. | @studnicky/virtual-fs |
AsyncFileSystemInterface | Defines durable asynchronous file operations. | @studnicky/virtual-fs |
NodeFileSystem | Provides Node promise-based filesystem operations. | @studnicky/virtual-fs/node |
OpfsFileSystem | Provides native browser Origin Private File System operations. | @studnicky/virtual-fs/browser |
OpfsFileSystemOptionsInterface | Defines OPFS construction options. | @studnicky/virtual-fs/browser |
OpfsStorageInterface | Defines the injected OPFS storage boundary. | @studnicky/virtual-fs/browser |
VirtualFileSystem | Provides virtual file system functionality. | @studnicky/virtual-fs |
VirtualFileSystemError | Represents virtual file system failures. | @studnicky/virtual-fs |