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

zip-writer

v2.2.0

Published

A modern streaming ZIP archive writer for JavaScript that uses the Web Streams API and Compression Streams API.

Readme

zip-writer

npm version GitHub CI codecov bundle size

A modern streaming ZIP archive writer for JavaScript that uses the Web Streams API and Compression Streams API.

Features

  • Streaming API - Write ZIP archives without buffering entries our the output zip into memory
  • Browser & Node.js - Works in both environments with the same API
  • Small bundle size - ~3KB minified and gzipped
  • Minimal dependencies - Only depends on p-mutex for mutex locking (adds ~390 bytes)
  • ZIP64 support - Automatic handling of large files and archives
  • Editable Central Directory - Reorder, rename, or remove entries before finalizing
  • 100% test coverage - Output validated against standard ZIP tools

Installation

npm install zip-writer

Basic Usage

import { ZipWriter } from "zip-writer";

const zipWriter = new ZipWriter();

// Pipe the ZIP output somewhere
zipWriter.readable.pipeTo(writableStream);

// Add entries to the ZIP
const data = new TextEncoder().encode("Hello, World!");
const info = await zipWriter.addEntry({
  readable: new ReadableStream({
    start(controller) {
      controller.enqueue(data);
      controller.close();
    },
  }),
  name: "hello.txt",
});
console.log(info);

// Finalize the ZIP archive
await zipWriter.finalize();

Helper Functions

readableFromBytes()

A helper function for creating a ReadableStream from a Uint8Array with proper backpressure handling. This is useful when you need to convert byte data into a stream for use with addEntry(). On supported platforms, you can use ReadableStream.from() instead.

This is not included in the main bundle to keep the size small.

Import:

import { readableFromBytes } from "zip-writer/readable-from-bytes";

Parameters:

  • data: Uint8Array - The byte array to convert to a stream

Returns: ReadableStream<Uint8Array> - A readable stream that chunks the data with proper backpressure

Example:

import { ZipWriter } from "zip-writer";
import { readableFromBytes } from "zip-writer/readable-from-bytes";

const zipWriter = new ZipWriter();

const data = new TextEncoder().encode("Hello, World!");
await zipWriter.addEntry({
  readable: readableFromBytes(data),
  name: "hello.txt",
});

await zipWriter.finalize();

API Reference

ZipWriter

The main class for creating ZIP archives.

Constructor

new ZipWriter(options?)

Parameters:

  • options.crc32?: (data: Uint8Array, value?: number) => number - Optional CRC32 function to use. Defaults to zlib.crc32 on Node.js and a pure JavaScript implementation in browsers.

Properties

readable: ReadableStream<Uint8Array>

The readable stream containing the ZIP archive data. Pipe this to a file, HTTP response, or any other writable destination.

// Save to file (Node.js with fs/promises)
import { createWriteStream } from "fs";
import { Writable } from "stream";

zipWriter.readable.pipeTo(Writable.toWeb(createWriteStream("archive.zip")));

// Send as HTTP response (in a web server)
return new Response(zipWriter.readable, {
  headers: {
    "Content-Type": "application/zip",
    "Content-Disposition": 'attachment; filename="archive.zip"',
  },
});
entries(): Promise<EntryInfo[]>

Returns a promise that resolves to an array of all entries that have been written to the archive. Each entry contains metadata like size, CRC32, compression info, etc.

Methods

addEntry(entry: Entry): Promise<EntryInfo>

Add an entry to the ZIP archive. Returns a promise that resolves to the entry info once written.

Parameters:

  • entry.readable: ReadableStream<ArrayBufferView> - Readable stream of entry data
  • entry.name: string - Entry name including internal path (required)
  • entry.comment?: string - Entry comment
  • entry.date?: Date - Entry date (defaults to current date)
  • entry.mode?: number - Entry permissions (Unix-style mode)
  • entry.store?: boolean - Set to true to disable compression (defaults to false, using DEFLATE compression)

Examples:

// Stream from a fetch response
const response = await fetch(imageUrl);
await zipWriter.addEntry({
  readable: response.body,
  name: "images/photo.jpg",
});

// Stream from a Uint8Array
const data = new TextEncoder().encode("Hello, World!");
await zipWriter.addEntry({
  readable: new ReadableStream({
    start(controller) {
      controller.enqueue(data);
      controller.close();
    },
  }),
  name: "hello.txt",
});

// With custom date and permissions
await zipWriter.addEntry({
  readable: scriptStream,
  name: "script.sh",
  date: new Date("2024-01-01"),
  mode: 0o755, // executable
});

// Disable compression for already-compressed files
await zipWriter.addEntry({
  readable: videoStream,
  name: "video.mp4",
  store: true, // no compression
});
finalize(options?): Promise<{ zip64: boolean, uncompressedEntriesSize: bigint, compressedEntriesSize: bigint, fileSize: bigint }>

Finalize the ZIP archive by writing the central directory and end of central directory records. This must be called after all entries have been added. This will await any pending entries to finish writing before finalizing.

Parameters:

  • options.entries?: ReadonlyArray<Readonly<EntryInfo>> - (Advanced) Override the entries to write in the central directory. This can be used to reorder, remove, or rename entries before finalizing the archive. You cannot change the offset, CRC32, compressed size, or uncompressed size of entries - only the name, comment, date, and order.

Returns:

  • zip64: boolean - Whether the archive uses ZIP64 format
  • uncompressedEntriesSize: bigint - Total uncompressed size of all entries
  • compressedEntriesSize: bigint - Total compressed size of all entries
  • fileSize: bigint - Total size of the ZIP file
const result = await zipWriter.finalize();
console.log(`Created ${result.zip64 ? "ZIP64" : "standard"} archive`);
console.log(`File size: ${result.fileSize} bytes`);
console.log(
  `Compression ratio: ${Number(result.compressedEntriesSize) / Number(result.uncompressedEntriesSize)}`,
);

Example with entries option:

// Get current entries and modify them
const entries = await zipWriter.entries();

// Remove an entry
const filtered = entries.filter((e) => e.name !== "temp.txt");

// Rename entries
const renamed = entries.map((e) => ({
  ...e,
  name: e.name.replace(/^old\//, "new/"),
}));

// Sort entries alphabetically
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));

// Finalize with modified entries
await zipWriter.finalize({ entries: sorted });

EntryInfo

Returned by ZipWriter.addEntry(). Contains metadata about a written entry.

Properties:

  • name: string - Entry name
  • comment?: string - Entry comment
  • date?: Date - Entry date
  • mode?: number - Entry permissions
  • store?: boolean - Whether compression was disabled
  • startOffset: bigint | number - Byte offset in the archive
  • crc32: number - CRC32 checksum
  • uncompressedSize: bigint | number - Uncompressed size in bytes
  • compressedSize: bigint | number - Compressed size in bytes
  • zip64: boolean - Whether this entry uses ZIP64 format

Example:

const info = await zipWriter.addEntry({
  readable: dataStream,
  name: "test.txt",
});
console.log(
  `Wrote ${info.name}: ${info.uncompressedSize} bytes (compressed to ${info.compressedSize})`,
);

Complete Example

import { ZipWriter } from "zip-writer";
import { createWriteStream } from "fs";
import { Writable } from "stream";

async function createZip() {
  const zipWriter = new ZipWriter();

  // Pipe output to file
  const fileStream = Writable.toWeb(createWriteStream("output.zip"));
  zipWriter.readable.pipeTo(fileStream);

  // Add a text file
  const data1 = new TextEncoder().encode("This is a readme");
  await zipWriter.addEntry({
    readable: new ReadableStream({
      start(controller) {
        controller.enqueue(data1);
        controller.close();
      },
    }),
    name: "readme.txt",
  });

  // Add files from URLs
  const imageResponse = await fetch("https://example.com/image.png");
  await zipWriter.addEntry({
    readable: imageResponse.body,
    name: "images/photo.png",
  });

  // Add a JSON file
  const data = { hello: "world" };
  const jsonData = new TextEncoder().encode(JSON.stringify(data, null, 2));
  await zipWriter.addEntry({
    readable: new ReadableStream({
      start(controller) {
        controller.enqueue(jsonData);
        controller.close();
      },
    }),
    name: "data.json",
  });

  // Wait for all entries and check results
  const entries = await zipWriter.entries();
  console.log(`Added ${entries.length} entries`);

  // Finalize the archive
  const result = await zipWriter.finalize();
  console.log(`Created archive: ${result.fileSize} bytes`);
}

createZip().catch(console.error);

Browser Usage

// Create a ZIP and trigger download
const zipWriter = new ZipWriter();

// Add entries...
const data = new TextEncoder().encode("Hello");
await zipWriter.addEntry({
  readable: new ReadableStream({
    start(controller) {
      controller.enqueue(data);
      controller.close();
    },
  }),
  name: "file.txt",
});
await zipWriter.finalize();

// Create download link
const blob = await new Response(zipWriter.readable).blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "archive.zip";
a.click();
URL.revokeObjectURL(url);

Benchmarks

Run using npm run bench. On a MacBook Pro (M2 Pro, 2023), Node.js 20.19.0:

Small files (10 × 10KB)

| Library | ops/sec | Relative Speed | | --------------------------- | ------- | -------------- | | zip-writer (@node-rs/crc32) | 721.94 | fastest | | zip-writer | 674.57 | 1.07x slower | | zip-writer (js crc32) | 659.90 | 1.09x slower | | fflate | 553.00 | 1.31x slower | | archiver | 338.14 | 2.14x slower | | @zip.js/zip.js | 279.19 | 2.59x slower |

Medium files (100 × 100KB)

| Library | ops/sec | Relative Speed | | --------------------------- | ------- | -------------- | | zip-writer | 23.91 | fastest | | zip-writer (@node-rs/crc32) | 22.91 | 1.04x slower | | zip-writer (js crc32) | 18.93 | 1.26x slower | | archiver | 18.10 | 1.32x slower | | @zip.js/zip.js | 11.65 | 2.05x slower | | fflate | 10.27 | 2.33x slower |

Large files (5 × 10MB)

| Library | ops/sec | Relative Speed | | --------------------------- | ------- | -------------- | | zip-writer | 7.22 | fastest | | zip-writer (@node-rs/crc32) | 6.78 | 1.06x slower | | archiver | 5.65 | 1.28x slower | | zip-writer (js crc32) | 4.82 | 1.50x slower | | fflate | 4.20 | 1.72x slower | | @zip.js/zip.js | 4.00 | 1.81x slower |

Many files (1000 × 1KB)

| Library | ops/sec | Relative Speed | | --------------------------- | ------- | -------------- | | fflate | 10.08 | fastest | | zip-writer | 6.89 | 1.46x slower | | zip-writer (@node-rs/crc32) | 5.94 | 1.70x slower | | zip-writer (js crc32) | 5.63 | 1.79x slower | | archiver | 4.98 | 2.02x slower | | @zip.js/zip.js | 2.93 | 3.44x slower |

Note: These benchmarks vary quite a bit, and they aren't an indication that any library is "better" than any other. For ZipWriter, these exist as a check to ensure that performance isn't significantly worse than other libraries.

ZIP64 Support

The library automatically uses ZIP64 format when needed:

  • Archives with more than 65,535 entries
  • Files larger than 4GB
  • Central directory larger than 4GB
  • Central directory offset greater than 4GB

No special configuration is needed - it's handled automatically.

Error Handling

try {
  await zipWriter.addEntry({ readable: someStream, name: "test.txt" });
} catch (error) {
  console.error("Failed to write entry:", error);
  // The ZIP stream is aborted on error - cannot add more entries
}

Once an error occurs during entry writing, the ZIP stream is in the errored state and cannot be recovered. You must create a new ZipWriter instance.

License

MIT