zip-writer
v2.2.0
Published
A modern streaming ZIP archive writer for JavaScript that uses the Web Streams API and Compression Streams API.
Maintainers
Readme
zip-writer
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-writerBasic 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 tozlib.crc32on 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 dataentry.name: string- Entry name including internal path (required)entry.comment?: string- Entry commententry.date?: Date- Entry date (defaults to current date)entry.mode?: number- Entry permissions (Unix-style mode)entry.store?: boolean- Set totrueto disable compression (defaults tofalse, 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 formatuncompressedEntriesSize: bigint- Total uncompressed size of all entriescompressedEntriesSize: bigint- Total compressed size of all entriesfileSize: 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 namecomment?: string- Entry commentdate?: Date- Entry datemode?: number- Entry permissionsstore?: boolean- Whether compression was disabledstartOffset: bigint | number- Byte offset in the archivecrc32: number- CRC32 checksumuncompressedSize: bigint | number- Uncompressed size in bytescompressedSize: bigint | number- Compressed size in byteszip64: 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
