npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vfs-kit

v1.0.4

Published

A headless-first Virtual File System for React. Complete with drag-and-drop hierarchy, tab management and pluggable storage adapters.

Downloads

550

Readme

vfs-kit

A headless virtual filesystem engine for React. Provides a fully typed adapter model, a caching engine, and a suite of hooks and renderless components for building file explorers, editors, and asset managers — with no opinions on styling.


Table of Contents


Installation

npm install vfs-kit

Peer dependencies: react, react-dom, @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities.

For IndexedDBAdapter: idb.


Quick Start

import { VfsProvider } from 'vfs-kit';
import { InMemoryAdapter } from 'vfs-kit/adapters';
import { FileTree } from 'vfs-kit/components';
import { useVfsEngine } from 'vfs-kit/hooks';

const adapter = new InMemoryAdapter();

export default function App() {
    return (
        <VfsProvider
            workspaces={[{ id: 'main', name: 'My Files', adapter }]}
            config={{ sessionId: 'user-1' }}
        >
            <Explorer />
        </VfsProvider>
    );
}

function Explorer() {
    const { fs } = useVfsEngine('main');

    return (
        <FileTree
            workspaceId="main"
            renderNode={({ flatNode, onClick, dragHandleProps, style }) => (
                <div {...dragHandleProps} onClick={onClick} style={style}>
                    {flatNode.node.name}
                </div>
            )}
        />
    );
}

Adapters

Adapters implement the VfsAdapter abstract class and provide the storage backend. Three are included; you can write your own.

InMemoryAdapter

Stores everything in Maps. No setup required. Ideal for prototyping, testing, and ephemeral state.

import { InMemoryAdapter } from 'vfs-kit/adapters';

const adapter = new InMemoryAdapter();

Supports history (supportsHistory = true). Data is lost on page refresh.

You can pre-seed the adapter directly before passing it to VfsProvider:

const root = await adapter.createFolder({ parentId: null, name: 'src' });
const file = await adapter.createFile({ parentId: root.id, name: 'index.ts', mimeType: 'text/typescript' });
await adapter.writeFile(file.id, new TextEncoder().encode('export {}'));

IndexedDBAdapter

Persists to the browser's IndexedDB via idb. Survives page refresh. Supports optional cross-tab sync via BroadcastChannel.

import { IndexedDBAdapter } from 'vfs-kit/adapters';

const adapter = new IndexedDBAdapter({
    dbName:    'my-app-fs',
    dbVersion: 1,              // optional, default 1
    isolation: 'shared',       // 'private' (default) | 'shared' — enables BroadcastChannel sync
});

await adapter.open(); // must be called before passing to VfsProvider

Call adapter.close() on unmount if managing lifecycle manually.

Supports history (supportsHistory = true). Enforces a 50-snapshot cap per file, evicting the oldest automatically.

RestAdapter

Delegates all operations to a remote HTTP API. Connects to a server-sent events (SSE) endpoint for real-time change propagation, with an optional fallback for non-SSE environments.

import { RestAdapter } from 'vfs-kit/adapters';

const adapter = new RestAdapter({
    baseUrl:          'https://api.example.com/fs',
    supportsHistory:  true,       // default true
    fetch:            customFetch, // optional — defaults to globalThis.fetch
    endpoints:        { ... },    // optional — override individual endpoint URLs
    realtimeFallback: (onMessage, onError) => {
        // e.g. set up a WebSocket and return a cleanup function
        return () => { /* disconnect */ };
    },
});

await adapter.open();

Default endpoint conventions (all relative to baseUrl):

| Operation | Method | Path | |---|---|---| | Get node | GET | /nodes/:id | | Get children | GET | /nodes/:parentId/children | | Create file | POST | /nodes/file | | Create folder | POST | /nodes/folder | | Write content | PUT | /content/:id | | Move (batch) | POST | /nodes/batch/move | | Get snapshots | GET | /snapshots/:fileId | | Save snapshot | POST | /snapshots/:fileId | | Restore snapshot | POST | /snapshots/:fileId/:index/restore | | Delete snapshot | DELETE | /snapshots/:fileId/:index | | SSE stream | GET | /events |

Any endpoint can be overridden individually via the endpoints option.

Writing a Custom Adapter

Extend VfsAdapter and implement all abstract methods. Set supportsHistory = true and implement the four history methods (getSnapshots, saveSnapshot, restoreSnapshot, deleteSnapshot) if your backend supports it.

import { VfsAdapter } from 'vfs-kit/core';

export class MyAdapter extends VfsAdapter {
    readonly supportsHistory = false;
    // implement abstract methods...
}

Provider

VfsProvider initialises the engine for each workspace and makes them available via context.

<VfsProvider
    workspaces={[
        { id: 'ws-a', name: 'Source', adapter: adapterA },
        { id: 'ws-b', name: 'Target', adapter: adapterB },
    ]}
    config={{
        sessionId:           'user-123',      // required — used for node locking
        allowDuplicateNames: false,           // default false
        duplicateResolution: 'throw',         // 'throw' | 'rename' — default 'throw'
        history: {
            maxSnapshots: 50,                 // default 50
            autosave: {
                enabled:    false,            // default false
                intervalMs: 30_000,
            },
        },
    }}
    cacheStrategy="hybrid"                    // 'eager' | 'lazy' | 'hybrid' — default 'hybrid'
    tabPersistence={{ strategy: 'session' }}  // 'session' | 'none' — default 'none'
    fallback={<div>Loading…</div>}
>
    {children}
</VfsProvider>

Cache strategies:

  • eager — loads all nodes on init
  • lazy — loads nodes on demand
  • hybrid — loads root children on init, everything else on demand

Hooks

useVfs

The all-in-one hook. Combines engine, tabs, selection, and expanded state in a single call. Use this when you want everything wired together; use the individual hooks when you need fine-grained control.

import { useVfs } from 'vfs-kit/hooks';

const {
    fs,        // VfsFsApi   — write operations
    tree,      // VfsTreeApi — read operations
    status,    // VfsStatusApi
    tabs,      // VfsTabsApi
    selection, // VfsSelectionApi
    expanded,  // VfsExpandedApi
} = useVfs({
    workspaceId: 'ws-a',           // optional — defaults to active workspace
    tabs:      { workspaceIds: ['ws-a'] },
    selection: { multiSelect: true, rangeSelect: true },
    expanded:  { persistKey: 'sidebar-expanded' },
});

useVfsEngine

Returns only the fs / tree / status surfaces for a workspace. The most commonly used hook when you don't need tabs or selection.

const { fs, tree, status } = useVfsEngine('ws-a');

fs — write operations:

fs.createFile(parentId, name, { mimeType?, meta? })
fs.createFolder(parentId, name, { meta? })
fs.rename(id, newName)
fs.delete(ids, permanent?)        // soft-delete by default
fs.restore(ids)
fs.purge(ids)                     // permanent delete
fs.move(ids, newParentId)
fs.write(id, content)             // Uint8Array
fs.lock(ids)
fs.unlock(ids)
fs.reorder(parentId, orderedIds)
fs.snapshot(fileId, label?)
fs.execute(command)               // raw VfsCommand escape hatch

tree — read operations:

tree.getNode(id)                  // Promise<VfsNode | null>
tree.getChildren(parentId, opts?) // Promise<VfsNode[]>
tree.getPath(id)                  // Promise<string>  e.g. '/src/index.ts'
tree.readFile(id)                 // Promise<Uint8Array>
tree.readJSON<T>(id)              // Promise<T>
tree.writeJSON<T>(id, data)       // Promise<void>
tree.search(query, opts?)
tree.getTrashed()
tree.getSnapshots(fileId)         // Promise<VfsFileSnapshot[]>

status:

status.pending  // boolean — true while a command is in flight
status.error    // Error | null — last command error
status.version  // number — increments on every change, useful as a cache key

useVfsTabs

Manages open file tabs. Supports dirty tracking, locking, drag-to-reorder, and optional session persistence.

import { useVfsTabs } from 'vfs-kit/hooks';

const tabs = useVfsTabs({
    workspaceIds: ['ws-a'],     // optional
    dirtyChecker: ({ savedContent, currentContent }) => ..., // optional custom checker
});
tabs.tabs           // VfsTab[]
tabs.activeTabId    // string | null
tabs.activeTab      // VfsTab | null

tabs.open(nodeId, workspaceId)   // opens or focuses a tab
tabs.close(tabId)
tabs.closeOthers(tabId)
tabs.closeAll(workspaceId?)
tabs.setActive(tabId)
tabs.reorder(activeId, overId)
tabs.lock(tabId)
tabs.unlock(tabId)
tabs.markDirty(tabId, currentContent)
tabs.markSaved(tabId)

VfsTab shape:

interface VfsTab {
    id:             string;
    nodeId:         string;
    workspaceId:    string;
    title:          string;
    isDirty:        boolean;
    isLocked:       boolean;
    savedContent:   Uint8Array | null;
    currentContent: Uint8Array | null;
    lastSavedAt:    number | null;
}

Important: when using a custom tab UI, use TabListBase and pass your useVfsTabs instance into it directly. Using TabList creates its own internal useVfsTabs instance which will be disconnected from yours.

useVfsHistory

Per-file snapshot manager. Loads snapshots for the active file and keeps the list up to date as snapshots are taken. Scoped to the current render — history is discarded when the component unmounts.

import { useVfsHistory } from 'vfs-kit/hooks';

const { snapshots, takeSnapshot, restore, remove, loading } = useVfsHistory({
    workspaceId: 'ws-a',
    nodeId:      'file-id',           // null clears the list
    onRestore:   (bytes) => {         // called after a restore so you can sync editor state
        setContent(new TextDecoder().decode(bytes));
    },
});
takeSnapshot(label?)     // saves current file content as a new snapshot
restore(index)           // writes snapshot content back to the file and calls onRestore
remove(index)            // removes a snapshot
loading                  // true while the initial snapshot list is loading
snapshots                // VfsFileSnapshot[] — most-recent first

VfsFileSnapshot shape:

interface VfsFileSnapshot {
    id:        string;
    fileId:    string;
    index:     number;
    content:   Uint8Array;
    createdAt: number;
    label?:    string;
}

Only available when the adapter has supportsHistory = true (InMemoryAdapter and IndexedDBAdapter both do; RestAdapter depends on your server).

useVfsExpanded

Manages folder expanded state in a file tree. Persists to sessionStorage when a persistKey is provided. Automatically collapses deleted folders.

const expanded = useVfsExpanded({
    workspaceIds:   ['ws-a'],            // optional
    defaultExpanded: ['folder-id'],      // optional
    persistKey:      'sidebar-expanded', // optional — enables sessionStorage persistence
});
expanded.expandedIds          // Set<string>
expanded.isExpanded(id)       // boolean
expanded.expand(id)
expanded.collapse(id)
expanded.toggle(id)
expanded.expandAll(ids)
expanded.collapseAll()
expanded.expandToNode(id, workspaceId?)  // expands all ancestors of a node

useVfsSelection

Manages selected node state. Supports single select, multi-select, and shift-range select.

const selection = useVfsSelection({
    multiSelect: true,  // default true
    rangeSelect: true,  // default true
});
selection.selection       // string[]
selection.lastSelectedId  // string | null
selection.isSelected(id)  // boolean
selection.select(id)      // replaces selection
selection.toggle(id)      // adds/removes from selection
selection.selectRange(orderedIds, anchorId, targetId)
selection.deselect(id)
selection.deselectAll()
selection.selectAll(ids)

useSortableTab

A thin wrapper around @dnd-kit/sortable's useSortable for tab drag-to-reorder. Returns ref, style, and dragHandleProps ready to spread onto a tab element.

import { useSortableTab } from 'vfs-kit/hooks';

function MyTab({ tab }) {
    const { ref, style, dragHandleProps } = useSortableTab(tab.id);
    return (
        <div ref={ref} style={style} {...dragHandleProps}>
            {tab.title}
        </div>
    );
}

Used internally by TabListBase when sortable is true. Use it directly when building a fully custom tab strip.


Components

All components are headless by default — they manage behaviour and delegate rendering to your own render props.

FileTree

Renders a flat virtualised list of nodes for a workspace. Handles expand/collapse, drag-and-drop reordering, inline creation, and drop-zone overlays.

import { FileTree } from 'vfs-kit/components';

<FileTree
    workspaceId="ws-a"
    draggable                         // enables drag-and-drop
    renderNode={({ flatNode, isSelected, isActive, isDragging, isDragSession,
                   isRenaming, setRenaming, dragHandleProps, style, onClick }) => (
        <div {...dragHandleProps} onClick={onClick} style={style}>
            {flatNode.node.name}
        </div>
    )}
    inlineCreate={{ kind: 'file' }}   // pass null to cancel
    onCancelCreate={() => {}}
    folderOverlay={{
        mode: 'full',                 // 'full' | 'none'
        renderOverlay: ({ isBlocked, isEmpty }) => <div />,
    }}
/>

NodeRenderProps:

interface NodeRenderProps<TMeta> {
    flatNode:        FlatNode<TMeta>;  // { node, level, isOpen }
    isSelected:      boolean;
    isActive:        boolean;
    isDragging:      boolean;
    isDragSession:   boolean;
    isRenaming:      boolean;
    setRenaming:     (v: boolean) => void;
    dragHandleProps: Record<string, unknown>;
    style:           React.CSSProperties;
    onClick:         (e: React.MouseEvent) => void;
}

When a file node is clicked, FileTree's internal onClick handles selection and expand — it does not open tabs. Wire tabs.open(node.id, workspaceId) inside your renderNode click handler if you want tab behaviour.

FileRenderer

Loads and renders the content of the active file. Re-runs when activeNodeId changes, handles loading and error states, and calls onLoad once content is available.

import { FileRenderer } from 'vfs-kit/components';

<FileRenderer
    workspaceId="ws-a"
    activeNodeId={tabs.activeTab?.nodeId ?? null}
    onLoad={(node, bytes) => setContent(new TextDecoder().decode(bytes))}
    onError={(node, error) => console.error(error)}
    fallback={<div>No file open</div>}
>
    {({ node, content, loading, error }) => (
        <textarea value={new TextDecoder().decode(content ?? new Uint8Array())} />
    )}
</FileRenderer>

Key the child component on node.id if it owns its own content state — this causes React to fully remount on tab switch, cleanly resetting state without a useEffect.

TabList / TabListBase

TabList is the higher-level component: it creates its own useVfsTabs instance internally.

TabListBase is the lower-level component: it accepts tab state as props, so you can drive it from a useVfsTabs instance you control elsewhere. Use TabListBase when you are also calling tabs.open() from your own code — otherwise the tab strip and your open calls will be in two disconnected states.

import { TabListBase } from 'vfs-kit/components';

// tabs = useVfsTabs({ workspaceIds: ['ws-a'] })

<TabListBase
    tabs={tabs.tabs}
    activeTabId={tabs.activeTabId}
    onReorder={tabs.reorder}
    onClose={tabs.close}
    onSetActive={tabs.setActive}
    onLock={tabs.lock}
    onUnlock={tabs.unlock}
    sortable                          // enables drag-to-reorder via @dnd-kit
>
    {({ tabs: tabList, activeTabId, onClose, onSetActive, onLock, onUnlock, dragHandleProps }) => (
        <div style={{ display: 'flex' }}>
            {tabList.map(tab => (
                <MyTab
                    key={tab.id}
                    tab={tab}
                    isActive={tab.id === activeTabId}
                    dragHandleProps={dragHandleProps(tab.id)}
                    onSetActive={onSetActive}
                    onClose={onClose}
                    onLock={onLock}
                    onUnlock={onUnlock}
                />
            ))}
        </div>
    )}
</TabListBase>

InlineInput

A controlled text input designed for inline rename flows. Commits on Enter or blur, cancels on Escape.

import { InlineInput } from 'vfs-kit/components';

<InlineInput
    initialValue={node.name}
    onSubmit={(newName) => fs.rename(node.id, newName)}
    onCancel={() => setRenaming(false)}
    className="my-input-class"
/>

Core Types

// vfs-kit/core

interface VfsNode<TMeta> {
    id:        string;
    name:      string;
    kind:      'file' | 'folder';
    parentId:  string | null;
    sortIndex: number | null;
    lockedBy:  string | null;
    createdAt: number;
    updatedAt: number;
    deletedAt: number | null;
    meta:      TMeta;
}

interface VfsFileNode<TMeta> extends VfsNode<TMeta> {
    kind:     'file';
    mimeType: string;
    size:     number;
}

interface VfsFolderNode<TMeta> extends VfsNode<TMeta> {
    kind: 'folder';
}

interface VfsFileSnapshot {
    id:        string;
    fileId:    string;
    index:     number;
    content:   Uint8Array;
    createdAt: number;
    label?:    string;
}

Roadmap

  • deleteSnapshot as a formal VfsCommand — currently remove() in useVfsHistory is local-state only; adding { op: 'deleteSnapshot', fileId, index } to VfsEngine and fs.deleteSnapshot() to VfsFsApi will make it durable
  • Autosave snapshotsVfsEngineConfig.history.autosave is wired in the config but not yet connected; a setInterval in useVfsHistory gated on autosave.enabled is the planned hookup point
  • Snapshot diff view — compare a snapshot's content against the current file before restoring