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

@lp-labs/zip-save

v0.1.1

Published

Create, read and download ZIP archives with native compression and no runtime dependencies.

Readme

@lp-labs/zip-save

Create ZIP archives, read their contents, and download files with one lightweight library. Inspired by the workflows of JSZip and FileSaver.js, without depending on either library or any other runtime package.

Framework-independent: use it with React, Vue, Angular, plain JavaScript, or Node.js. Includes ESM, CommonJS, and TypeScript declarations. Downloads require a browser; ZIP operations do not require a DOM.

The complete minified ESM build is approximately 6.6 KiB gzipped, with zero runtime dependencies. Source maps and development tools are not part of the application bundle; size may vary between releases.

Installation

npm install @lp-labs/zip-save

Requires a modern browser or Node.js 18+. Native raw DEFLATE support varies by browser and Node version; inspect supports.compression and supports.decompression when needed. No polyfills are bundled.

Browser Sandboxes

Version 0.1.1 uses .js entry points for compatibility with browser sandboxes and older package resolvers, while retaining both ESM and CommonJS support. If a sandbox reports a missing dist/index.cjs entry for version 0.1.0, update to version 0.1.1 or later and restart the sandbox to refresh its dependency cache.

ZIP And Download

import { zipAndSave } from '@lp-labs/zip-save';

await zipAndSave({
  'hello.txt': 'Hello world!',
  'reports/summary.json': JSON.stringify({ total: 42 }, null, 2),
}, 'reports.zip');

For files selected with an <input type="file" multiple>:

await zipAndSave(Array.from(fileInput.files), 'selected-files.zip');

File.webkitRelativePath is preserved when a directory picker supplies it. File descriptors are also supported:

await zipAndSave([
  { name: 'notes.txt', data: 'Some notes' },
  { name: 'image.png', data: imageBlob, options: { compression: 'STORE' } },
], 'bundle.zip', {
  onUpdate: ({ percent, currentFile }) => console.log(percent, currentFile),
});

Build An Archive

import Zip from '@lp-labs/zip-save';

const zip = new Zip();
zip.comment = 'Monthly export';
zip.file('hello.txt', 'Hello world!');
zip.file('bytes.bin', new Uint8Array([0, 128, 255]));
zip.folder('reports').file('summary.json', JSON.stringify({ total: 42 }));
zip.folder('empty-directory');

await zip.saveAs('export.zip');

Alternatively, generate an archive without downloading:

const blob = await zip.generateAsync({ type: 'blob' });
const bytes = await zip.generateAsync({ type: 'uint8array', compression: 'STORE' });
const base64 = await zip.generateAsync({ type: 'base64' });

Inputs accept strings, Blobs/Files, ArrayBuffers, typed-array views, and promises of those values. Strings are UTF-8 by default; set { base64: true } for padded base64 or { binary: true } for byte-valued strings.

Files And Folders

| API | Behavior | | --- | --- | | zip.file(name, data, options?) | Add or replace an entry; returns the current ZIP/folder view | | zip.file(name) | Get a file entry, or null | | zip.file(regexp) | Get matching files using paths relative to the current view | | zip.folder(name) | Create a directory and return a shared folder view | | zip.remove(name) | Remove a file or a directory and its descendants | | zip.forEach((path, entry) => ...) | Visit entries using paths relative to the current view | | zip.filter((path, entry) => ...) | Return matching entries | | zip.files | Snapshot of entries keyed by full archive paths | | zip.comment | Archive comment |

file options: date, comment, compression, base64, binary, dir, and createFolders (default true). Folder views share the same archive; generating a folder view produces an archive with paths relative to that folder. Empty directories are retained. Entry dates use ZIP's local-time DOS format, with two-second precision and years clamped to 1980-2107.

Read An Archive

import Zip from '@lp-labs/zip-save';

const zip = await Zip.loadAsync(zipFile, { checkCRC32: true });
const entry = zip.file('hello.txt');
if (entry) {
  const text = await entry.async('text');
  console.log(text);
}

zip.forEach((path, entry) => console.log(path, entry.dir));

Zip.loadAsync(data, options?) creates an archive. existingZip.loadAsync(...) merges validated entries into an existing archive, replacing entries with the same names. Loading is atomic: structural validation failures do not partially modify the destination.

entry.async(type?, options?) defaults to uint8array; options include signal, maxSize in bytes, and mimeType for Blob output. CRC32 and actual expanded size are checked whenever an imported entry is read. Set checkCRC32: true when loading to validate every entry immediately; otherwise decompression is lazy. Entry properties include name, dir, date, comment, and compression.

Outputs

Archive generation supports blob (default), uint8array, arraybuffer, base64, binarystring, and nodebuffer. Entry reads additionally support UTF-8 text and string. Node buffer output is a real Buffer at runtime and typed as Uint8Array to avoid requiring Node types in browser projects.

Save Individual Files

import { saveAs } from '@lp-labs/zip-save';

await saveAs(new Blob(['Hello!'], { type: 'text/plain;charset=utf-8' }), 'hello.txt');
await saveAs(new Uint8Array([1, 2, 3]), 'bytes.bin');
await saveAs('/exports/report.pdf', 'report.pdf');

String and URL inputs to saveAs are URLs, not file contents. Wrap text in a Blob to download it. URL downloads use fetch, require a successful HTTP response, and require CORS permission for cross-origin resources. HTTP, HTTPS, and blob URLs are accepted; fetch uses the browser's default same-origin credential behavior. Use saveAs(await response.blob(), filename) yourself for custom authentication or headers.

saveAs(data, filename?, options?) options:

| Option | Default | Behavior | | --- | --- | --- | | autoBom | false | Prepend a UTF-8 BOM to explicitly UTF-8 text/XML/JSON Blobs | | mimeType | application/octet-stream | MIME type for raw byte input; existing Blobs retain their type | | signal | none | Abort fetching or preparation before download dispatch | | revokeAfter | 40000 | Milliseconds before releasing the temporary object URL |

The filename is reduced to its leaf name and invalid/control characters are replaced. The helper removes its temporary anchor immediately and revokes its object URL later. Its promise resolves when the browser download is dispatched, not when writing to disk finishes. It cannot confirm download completion, choose a filesystem path, or cancel a download already dispatched.

Initiate downloads from user actions such as a button click. Browser policies, sandboxed frames, multiple-download limits, and mobile Safari may restrict asynchronous downloads or open a preview instead. For browsers that require immediate user activation, generate the Blob first and offer a separate download button. This package does not promise to bypass browser restrictions or provide legacy IE support.

Compression And Progress

generateAsync(options?, onUpdate?) and zip.saveAs(filename?, options?) support:

| Option | Default | Behavior | | --- | --- | --- | | compression | DEFLATE | Native raw DEFLATE when available; STORE writes uncompressed data | | compressionFallback | STORE | Use STORE without native compression; set error to require compression | | comment | zip.comment | Archive comment | | mimeType | application/zip | Blob output MIME type | | signal | none | Abort native streams and stop between entries/input conversion stages |

Each file can override compression. Even when DEFLATE is requested, empty entries and data that would grow after compression use STORE. The platform selects compression settings; custom compression levels are not supported.

onUpdate({ percent, currentFile }) reports entry-based progress, not byte-level progress. Pass it as the second argument to generateAsync, or in the options for saveAs/zipAndSave. zipAndSave returns the constructed Zip after dispatching the download.

import { supports } from '@lp-labs/zip-save';

console.log(supports.compression, supports.decompression, supports.download);

Reading DEFLATE requires native DecompressionStream('deflate-raw'). Without it, STORE archives can still be read, but reading a DEFLATE entry throws. Native support for deflate or gzip alone is not sufficient. No custom DEFLATE implementation is bundled.

Limits And Compatibility

The package implements standard single-disk ZIP records with UTF-8/ASCII filenames, STORE and DEFLATE, comments, directories, and central-directory sizes, including ordinary data-descriptor archives. It is a focused alternative, not a drop-in replacement for every JSZip or FileSaver.js API.

Not supported: ZIP64, encryption/passwords, split archives, symbolic links, legacy non-UTF-8 filename encodings, other compression algorithms, filesystem extraction, streaming archive output, and automatic file-picker dialogs. ZIP extra fields and filesystem permissions are not preserved when regenerating archives. Entry names and comments are UTF-8; comments with invalid UTF-8 are rejected.

Unsafe paths (absolute paths, control characters, or traversal segments), duplicate normalized names, overlapping entries, inconsistent headers, and malformed structures are rejected. CRC32 detects corruption; it does not authenticate archives.

Default byte limits apply to both reading and generation:

| Option | Default | | --- | --- | | maxArchiveSize | 256 MiB | | maxEntrySize | 128 MiB | | maxTotalSize | 256 MiB uncompressed | | maxEntries | 10,000 |

Override limits in loadAsync, generateAsync, or ZIP download options. Only nonnegative safe integers are accepted. Expanded data is bounded during decompression and checked against declared size. Entry reads inherit the loaded archive's per-entry bound unless overridden explicitly.

The implementation works in memory and can hold several copies of data during generation. These limits are not guarantees of peak memory usage. Archives must stay below ZIP32's 4 GiB field limits and 65,535 entries; use a different tool for very large archives. Cancellation cannot interrupt a synchronous conversion/CRC calculation or force an unresolved input promise to settle. Process large or untrusted archives in a worker and choose conservative limits appropriate to your application.

Support

Support maintenance with a donation via PayPal.

Author

Created by Libin Prasanth.