Skip to content

@studnicky/virtual-fs ​

In-memory synchronous filesystem primitive. Gives filesystem-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/node exposes Node filesystem APIs and @studnicky/virtual-fs/browser exposes browser APIs.

@studnicky/virtual-fs/node exports VirtualFileSystem, 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 ​

Testing a supplier catalogue import shouldn't require touching the real disk — VirtualFileSystem gives Northstar's test suite a filesystem that behaves exactly like Node's fs, entirely in memory. Create an instance with VirtualFileSystem.create(options?), seed it with starting files, and then call the same synchronous methods you'd already reach for: writeFileSync, readFileSync, renameSync, readdirSync, statSync. The example below seeds one file, writes a second, reads both back, renames one, lists the directory, and stats the result — all without a single real file touched.

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 ​

Run it and watch one VirtualFileSystem instance walk through a realistic import: it starts seeded with /data/hello.txt, writes config.json alongside it, renames that file to settings.json, lists everything sitting in /data, and stats the renamed file to confirm it's really there. Every step is checked by an assertion, so the demo doubles as proof the in-memory filesystem behaves exactly like the real one would.

Loading example…

Lifecycle hooks ​

Suppose Northstar wants a full audit trail of every file touched during a book-metadata import, without hard-coding logging calls into the filesystem logic itself. TracingVfs subclasses VirtualFileSystem and overrides all five lifecycle hooks — onCreate, onWrite, onRead, onRename, and onDelete — to record each one instead. Run the demo to see every operation in a realistic import walk through its matching hook: a fresh file triggers onCreate, overwriting it triggers onWrite, and reading, renaming, and deleting each fire their own event in a full trace.

Loading example…

Origin Private File System ​

A bookseller drafting an import list in the browser needs somewhere durable to stash it between page loads — somewhere that isn't a server round-trip and isn't localStorage. OpfsFileSystem implements the same asynchronous durable-file contract through the browser's native Origin Private File System, so the demo can create a directory, write a file into it, read the directory listing and the file back, and clean up — all running against real OPFS storage in your browser, no server involved.

Loading example…

Observability hooks ​

Every meaningful filesystem event — a file created, overwritten, read, renamed, or deleted — has a matching protected hook ready to be overridden, so Northstar can inject trace logging, metrics, or other side-effects at exactly the stage that matters. Keep overrides fast and non-blocking; a hook that throws is contained so the real filesystem operation still succeeds regardless.

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/node';
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 {
  static override create(): TracingVfs {
    return new TracingVfs({});
  }
  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/interfaces";
import { VirtualFileSystem } from "@studnicky/virtual-fs/node";

// 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, exported from @studnicky/virtual-fs/interfaces. 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/interfaces";

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 ​

@studnicky/virtual-fs/node exports VirtualFileSystem and VirtualFileSystemError; FileSystemInterface is available from @studnicky/virtual-fs/interfaces. 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";

What it is ​

@studnicky/virtual-fs is a filesystem boundary with a synchronous in-memory implementation and runtime-specific durable adapters. It provides files, directories, metadata, and contracts for code that needs filesystem access; it does not provide a document repository, synchronisation product, or durable service policy.

What it is for ​

Northstar Books uses this package to test import and export logic without the host filesystem, to run server-side durable file work through Node, or to store browser-side drafts through Origin Private File System. Node and browser are runtime-specific alternatives. Entities validate file data and interfaces let Northstar accept a file-system port without coupling book-processing logic to an adapter.

Northstar Books examples ​

  • VirtualFileSystem factory — seed, write, rename, readdir, stat solves the “test a supplier catalogue-file import without creating real files” problem. It seeds and mutates an in-memory directory, proving that Northstar can exercise familiar filesystem behaviour in an isolated test.
  • Observed VirtualFileSystem — lifecycle hook trace solves the “record every file operation during a book-metadata import” problem. It traces create, write, read, rename, and delete operations, proving that Northstar can add observation without embedding metrics in filesystem logic.
  • OpfsFileSystem — browser-native durable files solves the “retain a bookseller’s offline import draft in the browser” problem. It uses the browser-native durable adapter through the public contract, proving that Northstar can select an environment-appropriate backing store.

Public entrypoints ​

Import pathUse it when
@studnicky/virtual-fs/nodeNorthstar uses an in-memory filesystem or Node durable-file adapter in server and test code.
@studnicky/virtual-fs/browserNorthstar uses the browser Origin Private File System adapter for local durable files.
@studnicky/virtual-fs/entitiesNorthstar validates file-entry data at a filesystem boundary.
@studnicky/virtual-fs/interfacesNorthstar accepts synchronous or asynchronous filesystem capabilities through contracts.

Exports ​

SymbolPurposeImport path
FileSystemInterfaceDefines the synchronous in-memory file system contract.@studnicky/virtual-fs/interfaces
AsyncFileSystemInterfaceDefines durable asynchronous file operations.@studnicky/virtual-fs/interfaces
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/node
VirtualFileSystemErrorRepresents virtual file system failures. NodeFileSystem and OpfsFileSystem reject with it, carrying the platform fs error or DOMException as cause and its message.@studnicky/virtual-fs/node