Skip to content

@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

bash
pnpm add @studnicky/virtual-fs

Requires @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:

ts
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.

Loading example…

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.

Loading example…

Origin Private File System

OpfsFileSystem implements the asynchronous durable-file contract through the browser Origin Private File System API.

Loading example…

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.

HookWhen it firesArgs
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
ts
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:

typescript
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.

typescript
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.

Source on GitHub

Entities

@studnicky/virtual-fs/entities exports every schema namespace in src/entities.

typescript
import { EntryEntity } from '@studnicky/virtual-fs/entities';

Interfaces

@studnicky/virtual-fs/interfaces exports every TypeScript interface in src/interfaces, including configuration and state contracts.

typescript
import type { StatResultInterface } from '@studnicky/virtual-fs/interfaces';

Exports

SymbolPurposeImport path
FileSystemInterfaceDefines the synchronous in-memory file system contract.@studnicky/virtual-fs
AsyncFileSystemInterfaceDefines durable asynchronous file operations.@studnicky/virtual-fs
NodeFileSystemProvides Node promise-based filesystem operations.@studnicky/virtual-fs/node
OpfsFileSystemProvides native browser Origin Private File System operations.@studnicky/virtual-fs/browser
OpfsFileSystemOptionsInterfaceDefines OPFS construction options.@studnicky/virtual-fs/browser
OpfsStorageInterfaceDefines the injected OPFS storage boundary.@studnicky/virtual-fs/browser
VirtualFileSystemProvides virtual file system functionality.@studnicky/virtual-fs
VirtualFileSystemErrorRepresents virtual file system failures.@studnicky/virtual-fs