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

local-version-manager

v1.0.6

Published

A browser-based, framework-agnostic file version control library using the File System Access API. No server. No database. Just the browser.

Readme

local-version-manager

npm version License: MIT File System Access API

A browser-based, framework-agnostic file version control library powered by the File System Access API.
Automatically versions your files on the user's local disk — no server, no database, no backend required.


[!IMPORTANT]
This is NOT a UI library. It provides the core versioning and filesystem logic. You are responsible for building the UI components (buttons, file lists, previewers) that interact with this API.


When to Use It

| Use Case | Example | |----------|---------| | Contract / document management | Law firm tracking revisions of client agreements | | Design asset versioning | Studio keeping every iteration of a brand logo | | Report archival | Finance team auto-archiving monthly Excel reports | | Local-first web apps | Any app that needs offline, no-cloud file history | | Developer tools | Code snippet or config file versioning in-browser |


Browser Support

| Browser | Supported | |---------|-----------| | Chrome | ✅ v86+ | | Edge | ✅ v86+ | | Opera | ✅ v73+ | | Firefox | ❌ | | Safari | ❌ |

Secure context required: The File System Access API only works on HTTPS or localhost.
Always verify browser support with VersionManager.isSupported() at runtime.


Installation

npm install local-version-manager

Optional Peer Dependencies

Install only if you plan to call getFilePreview() on .docx or .xlsx files:

npm install mammoth xlsx

Quick Start

import VersionManager from "local-version-manager";

// 1. Initialize (validates browser support)
const vm = await VersionManager.initialize();

// 2. Select root folder — MUST be inside a user gesture (button click, etc.)
document.querySelector("#openBtn").addEventListener("click", async () => {
  await vm.selectRootDirectory();
});

// 3. Upload files (versioning is automatic)
document.querySelector("#fileInput").addEventListener("change", async (e) => {
  const results = await vm.uploadFiles(Array.from(e.target.files));
  // results → [{ name, versionName, versionNum, isNew }]
});

// 4. List files in current directory
const { files, folders } = await vm.listFiles();

// 5. Get version history
const versions = await vm.getVersions("report.pdf");

// 6. Restore a specific version
await vm.restoreVersion("report.pdf", 1);

// 7. Preview a file
const preview = await vm.getFilePreview("report.pdf");
// preview → { blob, url, strategy, mimeType, fileName }
// Always revoke when done:
vm.revokePreviewUrl(preview.url);

Step-by-Step Usage Guide

Step 1 — Initialize

import VersionManager from "local-version-manager";

let vm;

try {
  vm = await VersionManager.initialize();
} catch (err) {
  // Browser does not support File System Access API
  console.error("Unsupported browser:", err.message);
}

initialize() is a static factory method. It validates browser support and returns a configured VersionManager instance. Call once, then share the instance across your app.


Step 2 — Select Root Directory

// Must be called inside a browser user-gesture handler
button.addEventListener("click", async () => {
  const folderName = await vm.selectRootDirectory();
  if (folderName) {
    console.log("Working in:", folderName);
  }
  // Returns null if user cancels the picker
});

⚠️ User gesture required: showDirectoryPicker() (called internally) can only be invoked from a click, keydown, or similar user interaction. Calling it outside a gesture will throw a SecurityError.


Step 3 — Upload Files

// From a file input
const input = document.querySelector("#fileInput");
input.addEventListener("change", async () => {
  const results = await vm.uploadFiles(Array.from(input.files));

  results.forEach((r) => {
    console.log(`${r.name} → saved as ${r.versionName} (v${r.versionNum})`);
  });
});

// From drag-and-drop
dropzone.addEventListener("drop", async (e) => {
  e.preventDefault();
  const files = Array.from(e.dataTransfer.files);
  await vm.uploadFiles(files);
});

Each call to uploadFiles() writes the file to the current directory and saves a numbered version in .versions/. Uploading the same file twice creates file.1.ext then file.2.ext.


Step 4 — List Files

const { files, folders } = await vm.listFiles();

// files → FileEntry[]
files.forEach((f) => {
  console.log(`${f.name}  (${f.versionCount} versions, ${f.size} bytes)`);
});

// folders → FolderEntry[]
folders.forEach((d) => {
  console.log("Folder:", d.name);
});

The hidden .versions/ directory is always excluded from results.


Step 5 — Get Version History

const versions = await vm.getVersions("report.pdf");
// Sorted ascending: [{num:1,...}, {num:2,...}]

versions.forEach((v) => {
  console.log(`v${v.num} — ${v.size} bytes — ${v.lastModified}`);
});

Step 6 — Restore a Version

// Overwrites the main file with the content of version 1
// Does NOT create a new version entry
const restoredFrom = await vm.restoreVersion("report.pdf", 1);
console.log("Restored from:", restoredFrom); // "report.1.pdf"

Step 7 — Preview a File

const preview = await vm.getFilePreview("photo.png");
// preview.strategy → "image" | "pdf" | "text" | "docx" | "xlsx" | "open"

// Render based on strategy (your UI handles this):
if (preview.strategy === "image") {
  imgElement.src = preview.url;
} else if (preview.strategy === "pdf") {
  iframeElement.src = preview.url;
} else if (preview.strategy === "text") {
  // fetch the text from preview.url or use readBlobAsText(preview.blob)
}

// ⚠️ Always revoke to free memory:
vm.revokePreviewUrl(preview.url);

// Preview a specific version:
const oldPreview = await vm.getFilePreview("report.pdf", 2);

Vanilla JS Example

<!DOCTYPE html>
<html lang="en">
  <head><meta charset="UTF-8"><title>Version Manager Demo</title></head>
  <body>
    <button id="openBtn">Select Folder</button>
    <input type="file" id="fileInput" multiple />
    <ul id="fileList"></ul>
    <ul id="versionList"></ul>
    <div id="preview"></div>

    <script type="module">
      import VersionManager from "local-version-manager";

      const vm = await VersionManager.initialize();

      document.querySelector("#openBtn").addEventListener("click", async () => {
        const name = await vm.selectRootDirectory();
        if (!name) return;
        await renderFileList();
      });

      document.querySelector("#fileInput").addEventListener("change", async (e) => {
        await vm.uploadFiles(Array.from(e.target.files));
        await renderFileList();
      });

      async function renderFileList() {
        const { files } = await vm.listFiles();
        const list = document.querySelector("#fileList");
        list.innerHTML = files
          .map((f) => `<li data-name="${f.name}">${f.name} (${f.versionCount} versions)</li>`)
          .join("");

        list.addEventListener("click", async (e) => {
          const name = e.target.dataset.name;
          if (!name) return;
          await renderVersionHistory(name);
        });
      }

      async function renderVersionHistory(fileName) {
        const versions = await vm.getVersions(fileName);
        const list = document.querySelector("#versionList");
        list.innerHTML = versions
          .map(
            (v) =>
              `<li>v${v.num} — ${v.size} bytes 
                <button onclick="restore('${fileName}', ${v.num})">Restore</button>
                <button onclick="preview('${fileName}', ${v.num})">Preview</button>
              </li>`
          )
          .join("");
      }

      window.restore = async (fileName, num) => {
        await vm.restoreVersion(fileName, num);
        alert(`Restored ${fileName} to version ${num}`);
      };

      let activeUrl = null;
      window.preview = async (fileName, num) => {
        if (activeUrl) vm.revokePreviewUrl(activeUrl);
        const p = await vm.getFilePreview(fileName, num);
        activeUrl = p.url;
        const div = document.querySelector("#preview");
        if (p.strategy === "image") div.innerHTML = `<img src="${p.url}" />`;
        else if (p.strategy === "pdf") div.innerHTML = `<iframe src="${p.url}" width="100%" height="600"></iframe>`;
        else if (p.strategy === "text") {
          const text = await p.blob.text();
          div.innerHTML = `<pre>${text}</pre>`;
        }
      };
    </script>
  </body>
</html>

React Integration (Logic Only)

This example shows how to wire the library into React without any UI opinions. All rendering is yours.

// useVersionManager.js — custom hook
import { useState, useEffect, useRef } from "react";
import VersionManager from "local-version-manager";

export function useVersionManager() {
  const vmRef = useRef(null);
  const [ready, setReady] = useState(false);
  const [rootName, setRootName] = useState(null);
  const [files, setFiles] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    VersionManager.initialize()
      .then((instance) => {
        vmRef.current = instance;
        setReady(true);
      })
      .catch((err) => setError(err.message));
  }, []);

  const selectFolder = async () => {
    const name = await vmRef.current.selectRootDirectory();
    if (name) {
      setRootName(name);
      await refreshFiles();
    }
  };

  const refreshFiles = async () => {
    const { files: f } = await vmRef.current.listFiles();
    setFiles(f);
  };

  const upload = async (fileList) => {
    await vmRef.current.uploadFiles(Array.from(fileList));
    await refreshFiles();
  };

  const getVersions = (fileName) => vmRef.current.getVersions(fileName);

  const restore = async (fileName, versionNum) => {
    await vmRef.current.restoreVersion(fileName, versionNum);
    await refreshFiles();
  };

  const preview = (fileName, versionNum) =>
    vmRef.current.getFilePreview(fileName, versionNum ?? undefined);

  const revokeUrl = (url) => vmRef.current.revokePreviewUrl(url);

  return { ready, rootName, files, error, selectFolder, upload, getVersions, restore, preview, revokeUrl };
}
// App.jsx — consume the hook, own the UI completely
import { useState } from "react";
import { useVersionManager } from "./useVersionManager";

export default function App() {
  const { ready, rootName, files, error, selectFolder, upload, getVersions, restore, preview, revokeUrl } =
    useVersionManager();

  const [versions, setVersions] = useState([]);
  const [selectedFile, setSelectedFile] = useState(null);
  const [previewData, setPreviewData] = useState(null);

  if (!ready) return <p>Checking browser support…</p>;
  if (error) return <p style={{ color: "red" }}>Error: {error}</p>;

  const handleFileClick = async (fileName) => {
    setSelectedFile(fileName);
    const v = await getVersions(fileName);
    setVersions(v);
  };

  const handlePreview = async (fileName, versionNum) => {
    if (previewData?.url) revokeUrl(previewData.url);
    const p = await preview(fileName, versionNum);
    setPreviewData(p);
  };

  return (
    <div>
      {!rootName ? (
        <button onClick={selectFolder}>Select Folder</button>
      ) : (
        <p>Working in: <strong>{rootName}</strong></p>
      )}

      <input
        type="file"
        multiple
        onChange={(e) => upload(e.target.files)}
        disabled={!rootName}
      />

      {/* File list — your UI */}
      <ul>
        {files.map((f) => (
          <li key={f.name} onClick={() => handleFileClick(f.name)} style={{ cursor: "pointer" }}>
            {f.name} ({f.versionCount} versions)
          </li>
        ))}
      </ul>

      {/* Version history — your UI */}
      {selectedFile && (
        <ul>
          {versions.map((v) => (
            <li key={v.num}>
              v{v.num} — {v.size} bytes — {v.lastModified.toLocaleString()}
              <button onClick={() => restore(selectedFile, v.num)}>Restore</button>
              <button onClick={() => handlePreview(selectedFile, v.num)}>Preview</button>
            </li>
          ))}
        </ul>
      )}

      {/* Preview — your UI */}
      {previewData && previewData.strategy === "image" && (
        <img src={previewData.url} alt="preview" style={{ maxWidth: "100%" }} />
      )}
      {previewData && previewData.strategy === "pdf" && (
        <iframe src={previewData.url} width="100%" height="600" title="PDF Preview" />
      )}
      {previewData && previewData.strategy === "text" && (
        <TextPreview blob={previewData.blob} />
      )}
    </div>
  );
}

// Minimal text renderer helper
function TextPreview({ blob }) {
  const [text, setText] = useState("");
  blob.text().then(setText);
  return <pre style={{ overflow: "auto", maxHeight: 400 }}>{text}</pre>;
}

Integration Guide — Connecting Your Own UI

Connect a File Input

// HTML: <input type="file" id="upload" multiple />
document.querySelector("#upload").addEventListener("change", async (e) => {
  const results = await vm.uploadFiles(Array.from(e.target.files));
  // results contains { name, versionName, versionNum, isNew }
  // Trigger your UI refresh here
});

Display a File List

async function renderFiles() {
  const { files, folders } = await vm.listFiles();

  // files: [{ name, baseName, ext, size, lastModified, versionCount, handle }]
  // folders: [{ name, handle }]

  // Map to your UI components / state:
  setFileState(files);
  setFolderState(folders);
}

Show Version History

async function showHistory(fileName) {
  const versions = await vm.getVersions(fileName);
  // [{ num, name, size, lastModified, handle }] — sorted oldest → newest

  setVersionState(versions);
}

Trigger Restore

async function onRestoreClick(fileName, versionNum) {
  try {
    const restoredFrom = await vm.restoreVersion(fileName, versionNum);
    notify(`Restored from ${restoredFrom}`);
    await renderFiles(); // refresh your list
  } catch (err) {
    notify(`Restore failed: ${err.message}`);
  }
}

Handle Preview Rendering

async function onPreviewClick(fileName, versionNum = null) {
  const preview = await vm.getFilePreview(fileName, versionNum);

  switch (preview.strategy) {
    case "image":
      // Set <img src={preview.url} />
      break;
    case "pdf":
      // Set <iframe src={preview.url} />
      break;
    case "text":
      // const text = await preview.blob.text()
      break;
    case "docx":
      // Use renderDocxPreview(preview.blob, mammoth) from the library helpers
      break;
    case "xlsx":
      // Use renderXlsxPreview(preview.blob, XLSX) from the library helpers
      break;
    case "open":
      // File type not previewable in-browser — offer a download link instead
      const a = document.createElement("a");
      a.href = preview.url;
      a.download = preview.fileName;
      a.click();
      break;
  }

  // ⚠️ Revoke when done to free memory
  vm.revokePreviewUrl(preview.url);
}

Navigate Folders

// Enter a sub-folder
await vm.navigateInto("Invoices");

// Navigate back via breadcrumbs
const crumbs = vm.getBreadcrumbs();
// [{ name: "MyFolder", index: 0 }, { name: "Invoices", index: 1 }]
await vm.navigateTo(0); // back to root

// Get current path string
const path = vm.getCurrentPath(); // "MyFolder/Invoices"

API Reference

VersionManager.initialize()

Validates browser support and returns a new instance.

static initialize(): Promise<VersionManager>
// Throws if File System Access API is not supported

vm.selectRootDirectory()

Prompts user to pick a folder. Creates .versions/ inside it.

selectRootDirectory(): Promise<string | null>
// Returns folder name, or null if user cancels
// ⚠️ Must be called inside a user gesture (click, keydown, etc.)

vm.uploadFiles(files)

Uploads files to the current directory. Every upload creates a new version.

uploadFiles(files: File[]): Promise<UploadResult[]>

type UploadResult = {
  name: string;        // "report.pdf"
  versionName: string; // "report.2.pdf"
  versionNum: number;  // 2
  isNew: boolean;      // true only for the first upload
};

vm.listFiles()

Returns files and sub-folders in the current directory.

listFiles(): Promise<{ files: FileEntry[]; folders: FolderEntry[] }>

type FileEntry = {
  name: string;
  baseName: string;
  ext: string;
  size: number;
  lastModified: Date;
  versionCount: number;
  handle: FileSystemFileHandle;
};

type FolderEntry = {
  name: string;
  handle: FileSystemDirectoryHandle;
};

vm.getVersions(fileName)

Returns full version history, sorted ascending by version number.

getVersions(fileName: string): Promise<VersionEntry[]>

type VersionEntry = {
  name: string;           // "report.2.pdf"
  num: number;            // 2
  size: number;
  lastModified: Date;
  handle: FileSystemFileHandle;
};

vm.restoreVersion(fileName, versionNumber)

Overwrites the main file with the content of the specified version.
Does NOT create a new version entry.

restoreVersion(fileName: string, versionNumber: number): Promise<string>
// Returns the version file name restored from (e.g. "report.1.pdf")
// Throws if version does not exist

vm.getFilePreview(fileName, versionNumber?)

Returns a preview object. The caller is responsible for rendering.

getFilePreview(fileName: string, versionNumber?: number): Promise<PreviewResult>

type PreviewResult = {
  blob: Blob;
  url: string;        // Use in <img>, <iframe>, fetch(), etc.
  strategy: "image" | "pdf" | "text" | "docx" | "xlsx" | "open";
  mimeType: string;
  fileName: string;
};

⚠️ Always call vm.revokePreviewUrl(url) when done to avoid memory leaks.


vm.createFolder(folderName)

Creates a sub-folder and its .versions/ mirror.

createFolder(folderName: string): Promise<FileSystemDirectoryHandle>

vm.navigateInto(folderName) / vm.navigateTo(index)

Navigate the library's internal directory pointer.

navigateInto(folderName: string): Promise<void>
navigateTo(index: number): Promise<void>

Helper Methods

vm.getCurrentPath(): string           // "MyFolder/Invoices"
vm.getBreadcrumbs(): Breadcrumb[]     // [{ name, index }]
vm.revokePreviewUrl(url: string): void

Exported Utility Functions

These helpers are exported and can be imported directly if needed:

import {
  splitNameExt,        // Split "report.pdf" → { baseName: "report", ext: "pdf" }
  buildVersionFileName,// Build "report.2.pdf"
  parseVersionFileName,// Parse "report.2.pdf" → { num: 2 }
  formatFileSize,      // 1048576 → "1.0 MB"
  formatTimestamp,     // Date → "Mar 21, 10:45 AM"
  getMimeType,         // "photo.png" → "image/png"
  getFileIcon,         // "photo.png" → "image"
  getPreviewStrategy,  // "photo.png" → "image"
  readBlobAsText,      // blob → Promise<string>
  renderDocxPreview,   // blob, mammoth → Promise<html string>
  renderXlsxPreview,   // blob, XLSX → Promise<Sheet[]>
  STRATEGIES,          // { TEXT, IMAGE, PDF, DOCX, XLSX, OPEN, NONE }
} from "local-version-manager";

Best Practices

✅ HTTPS or localhost only

The File System Access API requires a secure context. Ensure your app is served over HTTPS in production.

// Check at runtime
if (!('showDirectoryPicker' in window)) {
  alert("File System Access API not supported. Use Chrome or Edge over HTTPS.");
}

✅ User gesture requirement

selectRootDirectory() must be called inside a user-triggered event handler:

// ✅ Correct
button.addEventListener("click", () => vm.selectRootDirectory());

// ❌ Wrong — will throw SecurityError
window.onload = () => vm.selectRootDirectory();

✅ Always revoke object URLs

Every call to getFilePreview() creates a blob: URL that holds memory. Free it when the preview is unmounted or replaced:

// React cleanup
useEffect(() => {
  return () => {
    if (previewUrl) vm.revokePreviewUrl(previewUrl);
  };
}, [previewUrl]);

// Vanilla JS
const preview = await vm.getFilePreview("file.png");
// ... use preview.url ...
vm.revokePreviewUrl(preview.url); // when no longer needed

✅ Handle errors gracefully

try {
  await vm.uploadFiles(files);
} catch (err) {
  if (err.name === "NotAllowedError") {
    // Permission denied — user needs to re-select the folder
  } else if (err.name === "AbortError") {
    // User cancelled the picker
  } else {
    console.error("Upload failed:", err);
  }
}

✅ Re-permission on page reload

The File System Access API requires the user to re-select the folder on every page load (permissions do not persist). Always check if rootHandle is set before calling other methods:

// Safe pattern
if (!rootName) {
  await vm.selectRootDirectory();
}

Storage Architecture

/user-selected-root/
├── report.pdf                ← main file (always the latest content)
├── photo.png
├── Invoices/                 ← user subfolder
│   └── invoice-001.pdf
└── .versions/                ← hidden; managed by the library
    ├── report/
    │   ├── report.1.pdf      ← first upload
    │   └── report.2.pdf      ← second upload
    ├── photo/
    │   └── photo.1.png
    └── Invoices/
        └── invoice-001/
            └── invoice-001.1.pdf

Rules:

  • The main file always has the original name and holds the latest content.
  • Versions live in .versions/{baseName}/{baseName}.{N}.{ext}.
  • restoreVersion() overwrites the main file only — no new version is appended.
  • Each sub-folder has its own isolated version history under .versions/.

Architecture Overview

local-version-manager/
├── src/
│   ├── index.js              ← Public API surface (VersionManager class)
│   ├── core/
│   │   ├── fileSystem.js     ← Low-level File System Access API wrappers
│   │   └── versionManager.js ← Mid-level versioning operations
│   └── utils/
│       └── helpers.js        ← Pure utilities: parsing, formatting, preview strategy
├── demo/                     ← Standalone HTML demo (not published)
├── dist/                     ← Built bundles (ESM + UMD, published to npm)
├── package.json
├── vite.config.js
└── README.md

Project Setup & Development

# Install dev dependencies
npm install

# Run the demo app locally (http://localhost:5173)
npm run dev

# Build the distributable bundles
npm run build

Publishing to npm

1. Prerequisites

2. Version Bump Strategy

Use Semantic Versioning:

| Change type | Command | Example | |-------------|---------|---------| | Bug fix | npm version patch | 1.0.01.0.1 | | New feature (backward compatible) | npm version minor | 1.0.01.1.0 | | Breaking change | npm version major | 1.0.02.0.0 |

3. Publish

# Log in to npm
npm login

# Build the distribution bundles
npm run build

# Bump version (choose one)
npm version patch     # bug fix
npm version minor     # new feature
npm version major     # breaking change

# Publish to npm registry
npm publish

# Verify it was published
npm view local-version-manager

4. Subsequent Releases

# Make your changes, then:
npm version patch      # or minor / major
npm run build
npm publish

Tip: Run npm pack --dry-run before publishing to verify exactly which files will be included in the package.


License

MIT © 2025