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

un7z-opfs

v1.0.2

Published

7z archive extraction straight into the browser's Origin Private File System, from inside a Web Worker

Readme

un7z-opfs

CI

Extracts .7z archives straight into the browser's Origin Private File System (OPFS), from inside a dedicated Web Worker, with minimal JS-side memory use.

Why this exists:

  • Runs entirely in the browser. No server-side unpacking, no upload/download round-trip — the archive is decompressed and written to OPFS by a WASM build of 7-Zip running in a Web Worker on the client.

  • Minimal memory footprint. Extracted bytes are written straight into a FileSystemSyncAccessHandle as 7-Zip decompresses each entry — nothing is buffered in JS/wasm memory for the extracted output. The only memory held is the input archive itself (kept in the WASM heap for the duration of extraction) and small per-chunk buffers as each write happens. A 2GB archive does not require 2GB of JS heap to extract.

  • .7z archives only (LZMA/LZMA2/XZ/BCJ/BCJ2/Delta/Copy) — no Zip/Rar/Tar/gzip. If you need those, use the full 7-Zip build instead.

  • Worker-only. OPFS's synchronous file I/O (FileSystemSyncAccessHandle) only exists inside dedicated Web Workers — this is a platform requirement, not a choice.

  • No COOP/COEP, no SharedArrayBuffer. The WASM module uses classic Emscripten Asyncify (not pthreads, not JSPI) to bridge 7-Zip's synchronous C++ code with OPFS's asynchronous handle-acquisition API, so it needs none of the cross-origin-isolation headers pthreads-based WASM usually requires.

Quick start

npm install un7z-opfs

This package ships plain, unbundled ESM files (worker/opfs-extractor.js + dist/7zz.js/dist/7zz.wasm) rather than a single bundled artifact, so the browser can load the worker directly via a URL — no bundler-specific worker loader required. The ./node_modules/... path below only resolves if whatever serves your page also serves node_modules over HTTP at that path, which dev servers like Vite/webpack-dev-server do by default, but a typical production static host (serving only dist//public/) does not. For production, either copy worker/opfs-extractor.js and dist/7zz.js/ dist/7zz.wasm into your served assets directory as a build step, or point your bundler's native worker syntax (e.g. new Worker(new URL('un7z-opfs/worker/opfs-extractor.js', import.meta.url), { type: 'module' })) at the package so it gets bundled and copied automatically.

// main.js (the page)
const worker = new Worker('./node_modules/un7z-opfs/worker/opfs-extractor.js', { type: 'module' });

worker.onmessage = (ev) => {
  const msg = ev.data;
  if (msg.type === 'progress') {
    console.log(`${msg.currentFile ?? ''}: ${msg.processedBytes}/${msg.totalBytes} bytes`);
  } else if (msg.type === 'done') {
    console.log('extraction complete');
  } else if (msg.type === 'error') {
    console.error('extraction failed:', msg.message);
  }
};

const archive = await (await fetch('/some-archive.7z')).arrayBuffer();
worker.postMessage({ type: 'extract', archive, destPath: 'my-extracted-archive' }, [archive]);
// after extraction, read the result back from the main thread
const root = await navigator.storage.getDirectory();
const dir = await root.getDirectoryHandle('my-extracted-archive');
const fileHandle = await dir.getFileHandle('some-file.txt');
const file = await fileHandle.getFile();
console.log(await file.text());

You can also import extractToOpfs() directly and compose it into your own worker script instead of using worker/opfs-extractor.js as the whole worker entry point:

import { extractToOpfs } from 'un7z-opfs/worker/opfs-extractor.js';

await extractToOpfs({
  archive: someArrayBuffer,
  destPath: 'my-folder',
  onProgress: (info) => console.log(info.processedBytes, '/', info.totalBytes),
});

Message protocol

Inbound (main thread → worker):

| field | type | notes | |---|---|---| | type | "extract" | | | id | string? | optional correlation id, echoed back on every response | | archive | ArrayBuffer | raw .7z bytes — transfer it (postMessage(msg, [archive])) to avoid a copy | | destPath | string | destination folder, relative to the OPFS root — see limitation below |

Outbound (worker → main thread):

| message | fields | |---|---| | { type: "progress", ... } | id?, processedBytes, totalBytes, currentFile (string \| null) | | { type: "done", ... } | id? | | { type: "error", ... } | id?, message, stack? |

totalBytes always reflects the archive's real uncompressed size — the extractor runs a header-only 7zz l -slt pass first specifically so progress is never reported against an unknown total.

OPFS path limitation

destPath is always relative to navigator.storage.getDirectory(). There is no way to pass in an arbitrary FileSystemDirectoryHandle you already have a reference to — this is a structural limitation of how Emscripten's filesystem mounting works, not something this project chose to omit. If you need the result under a specific handle, extract to a path under the OPFS root and then move/reference it from there.

Extraction always starts from an empty destination: this mount doesn't reflect pre-existing OPFS content at the destination path, so it can't detect or merge with files already there.

Building

Prerequisites:

  • Emscripten SDK, version 6.0.5 (this is what the patch and build flags were verified against — other versions may work but aren't guaranteed to)
  • CMake ≥ 3.20
emsdk install 6.0.5
emsdk activate 6.0.5
source /path/to/emsdk/emsdk_env.sh

cmake -S . -B build
cmake --build build

# copy the freshly built output over the committed dist/ used at runtime
cp build/dist/7zz.js build/dist/7zz.wasm dist/

Output lands at build/dist/7zz.js + build/dist/7zz.wasm. The repo ships pre-built copies at dist/7zz.js/dist/7zz.wasm so consumers don't need Emscripten just to npm install this package — rebuild and copy them over manually (as above) if you change src-glue/opfs_fs.js or patches/7zz-emcc.patch. The Testing section below (npm test) runs against whatever is currently in dist/, so re-run the copy step before testing local changes to the C++/glue side.

7-Zip's source itself is fetched by CMakeLists.txt (via ExternalProject_Add) from the ip7z/7zip GitHub releases (7-zip.org no longer hosts source archives for every version). The build compiles 7-Zip's own minimal Alone7z bundle (the 7zr binary's source set — LZMA/LZMA2/XZ/BCJ/BCJ2/Delta/Copy only, no Zip/Rar/Tar/Nsis handlers), patched via patches/7zz-emcc.patch to target Emscripten and to link in the custom OPFS filesystem backend (src-glue/opfs_fs.js).

Architecture notes

The custom FS backend (src-glue/opfs_fs.js) overrides exactly two Emscripten syscalls — __syscall_openat and __syscall_mkdirat — as async functions (using classic Asyncify's __async: 'auto' library metadata) that resolve and cache real OPFS FileSystemDirectoryHandle/ FileSystemSyncAccessHandles the first time a given path is touched. Everything else (read/write/close/llseek) runs as plain synchronous JS against an already-open handle, since those OPFS methods are synchronous by spec once a handle exists. This is what lets the whole thing avoid pthreads/JSPI/SharedArrayBuffer entirely.

Testing

npm install
npx playwright install --with-deps chromium
npm test

The suite (test/extract.spec.js) drives a real Chromium instance via Playwright — OPFS and Web Workers aren't available in Node, so this can't be a plain unit test. It's Chromium-only for now: Firefox/WebKit support for FileSystemSyncAccessHandle is newer and less consistently available.

Trying a local build in another project

To sanity-check a local build (or a change to src-glue/worker) inside a real consuming project, before publishing to npm:

# in this repo, after `cmake --build build` + copying dist/ (see Building above)
npm pack
# produces un7z-opfs-<version>.tgz in this directory

# in your consuming project
npm install /path/to/un7z-opfs/un7z-opfs-<version>.tgz

npm link works too (npm link here, then npm link un7z-opfs in the consuming project), but npm pack/npm install <tarball> is closer to what consumers actually get from the registry — it only includes the files listed in package.json's files field, so it also catches accidental "works locally, missing from the published package" bugs that a symlink via npm link would hide.

Publishing to npm

Prerequisites: an npm account with publish rights to un7z-opfs (npm login if you aren't already authenticated — npm whoami confirms).

# 1. make sure dist/ reflects the current source (see Building above)
cmake -S . -B build && cmake --build build
cp build/dist/7zz.js build/dist/7zz.wasm dist/

# 2. run the test suite against that build
npx playwright install --with-deps chromium
npm test

# 3. bump the version (updates package.json + package-lock.json, commits,
#    and tags -- run from a clean working tree)
npm version patch   # or: minor / major

# 4. sanity-check exactly what would be published
npm pack --dry-run

# 5. publish (the package is unscoped, so no --access flag is needed;
#    add --access public if you rename it to a scoped name like @you/un7z-opfs)
npm publish

# 6. push the version bump commit + tag created by `npm version`
git push && git push --tags

What actually gets published

npm includes only the paths listed in package.json's "files" array (plus package.json, README.md, and the license file, which npm always includes regardless of "files"). Verified with npm pack --dry-run against the current tree:

| path | purpose for a consumer | |---|---| | dist/7zz.js, dist/7zz.wasm | the compiled 7-Zip WASM module — what main points at and what worker/opfs-extractor.js imports | | worker/opfs-extractor.js | the ready-to-use worker entry point / extractToOpfs() export — what the Quick start examples above actually load | | index.d.ts | TypeScript types for extractToOpfs() and the message protocol | | src-glue/opfs_fs.js, patches/7zz-emcc.patch | build-time inputs only (the Emscripten FS backend source and the 7-Zip patch) — not imported at runtime by consumers, only needed if you're rebuilding the WASM module yourself per the Building section | | License.txt | required by the LGPL + unRAR terms this project inherits from 7-Zip |

patches/ and src-glue/ add a little dead weight to the published tarball for a pure consumer (they're only read by CMakeLists.txt), but are kept in files so npm pack/npm install <tarball>-based local testing (see above) exercises the exact same file set that a from-source rebuild depends on.

Differences from upstream 7z-wasm

un7z-opfs is a from-scratch rewrite of use-strict/7z-wasm (itself formerly published to npm as 7z-wasm), not a drop-in replacement:

  • Only .7z archives (upstream supports everything 7-Zip does: zip, rar, tar, gzip, bzip2, nsis, ...)
  • Browser Worker + OPFS only (upstream also supports Node.js via a CLI and NODEFS/WORKERFS)
  • CMake + ExternalProject_Add build (upstream uses a Dockerfile + bash scripts)
  • Single ESM build artifact (upstream ships both UMD and ESM)
  • Message-based extraction API with progress reporting (upstream exposes the raw Emscripten FS/callMain surface)

License

7-Zip itself is distributed under the GNU LGPL + unRAR restriction (see License.txt); this project's own code (the CMake build, src-glue/opfs_fs.js, worker/opfs-extractor.js, tests) follows the same terms as it's a derivative build of 7-Zip's source.