zip.js

A JavaScript library to zip and unzip files

Notable features

This library depends on the Promise, the TypedArray, the Streams APIs and these ones optionally:

Compatibility

This library works fully with the latest versions of Chrome, Firefox, Safari, Microsoft Edge, Node.js, Deno, and Bun. The minimum supported versions depend on the imported build, cf. the Library variants and Bundle size sections:

Build Chrome, Edge Firefox Safari Node.js Deno Bun
WebAssembly builds: index.js (default), lib/zip-core-wasm.js, dist/zip.min.js, dist/zip-fs.min.js 76 102 15.1 18 1.0 0.7
"native" JavaScript builds: index-native.js, lib/zip-core-native.js, dist/zip-native.min.js, dist/zip-fs-native.min.js 76 102 14.1 18 1.0 0.7
"core" builds: lib/zip-core.js, lib/zip-core-reader.js, lib/zip-core-writer.js, lib/zip-core-custom.js, dist/zip-core.min.js, dist/zip-fs-core.min.js 80 113 16.4 18 1.19 1.3.3

Reading untrusted zip files

zip.js validates untrusted input by default. The filenames returned by getEntries() are checked against path traversal: names containing .. segments between slashes or backslashes, absolute paths, drive letters or leading backslashes are rejected with an ERR_UNSAFE_FILENAME error. The data of an entry cannot be read outside the bounds of the zip file. Optional checks harden reading further: the checkOverlappingEntry option detects entries sharing bytes with other entries, the checkCrc32 option verifies the CRC-32 checksum of the extracted data, and strictness: "strict" detects duplicate filenames, ambiguous end of central directory records, and local file headers contradicting the central directory. The filename validation can be relaxed or disabled with the filenameValidation option; an application resolving paths from entry.filename, e.g. to write extracted files to disk, relies on it staying enabled.

Some checks remain the responsibility of the application:

The normalizeFilename option below rewrites the filenames for a Windows filesystem, the most restrictive common target. It replaces the characters Windows forbids, including the backslash, with _, removes the trailing dots and spaces Windows drops silently, and prefixes the reserved device names, with or without an extension. It runs before the validation and leaves the . and .. components alone, so path traversal is still rejected. Two entries rewritten to the same name are reported as an ERR_AMBIGUOUS_ARCHIVE error when checkAmbiguity is set. The stored name remains available in rawFilename. The File System Access API of Chromium on Windows also refuses names ending with .lnk, .local, .scf or .url.

import { ZipReader } from "@zip.js/zip.js";

const ILLEGAL_CHARACTERS = /[\x00-\x1f"*:<>?|\\]/g;
const TRAILING_DOTS_AND_SPACES = /[. ]+$/;
const RESERVED_NAMES = /^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³])(\..*)?$/i;

const zipReader = new ZipReader(reader, { normalizeFilename });

function normalizeFilename(filename) {
  return filename.split("/").map(normalizePathComponent).join("/");
}

function normalizePathComponent(component) {
  if (!component || component == "." || component == "..") {
    return component;
  }
  const result = component.replace(ILLEGAL_CHARACTERS, "_").replace(TRAILING_DOTS_AND_SPACES, "") || "_";
  return RESERVED_NAMES.test(result) ? "_" + result : result;
}

Demos

Documentation

Installation

You can get the code via:

Note: import * as zip imports the whole library. See the Bundle size section below to reduce the footprint of zip.js in your application.

You can also install manually the library by adding zip.min.js from the /dist directory in your project. Then, include zip.min.jsin your HTML page:

Bundle size

zip.js is tree-shakeable: import named identifiers, e.g. import { ZipReader, BlobReader, TextWriter } from "@zip.js/zip.js";, and your bundler removes what you do not use, e.g. the whole write path if you only read zip files.

Smaller entry points are also available, e.g. @zip.js/zip.js/lib/zip-core.js embeds neither the Web Worker code nor the WebAssembly module, pass workerURI and wasmURI to configure() instead. @zip.js/zip.js/lib/zip-core-custom.js goes further and embeds no deflate implementation at all, cf. Custom web workers and compression engines. The prebuilt bundles in /dist are standalone files for script tags and AMD loaders, they are not tree-shaken.

The @zip.js/zip.js/external entry point offers the full API but references the Web Worker code and the WebAssembly module as external files instead of embedding them. Bundlers like webpack and Vite detect these references, emit zip-web-worker.js and zip-module.wasm as separate assets, and remove approximately 59KB of embedded payloads from the main bundle. The worker also runs from a real file URL. This avoids blob: restrictions on pages and browser extensions with a strict Content Security Policy. With Vite, add @zip.js/zip.js to optimizeDeps.exclude to resolve the assets during development. With bundlers which do not rewrite new URL(..., import.meta.url) expressions, copy the two files next to the output bundle. The prebuilt bundle zip-fs-external.min.js offers the same behavior without a bundler, it resolves the two files from its own directory: deploy the three files together.

Smaller compositions of this entry point are also available: @zip.js/zip.js/lib/zip-fs-core-external.js excludes the MIME type table (approximately 23KB), getMimeType() then returns "application/octet-stream", and @zip.js/zip.js/lib/zip-core-external.js also excludes the filesystem API. Both come with prebuilt ES module bundles in /dist. The full MIME type table remains available from any entry point via @zip.js/zip.js/mime-types.

Library variants

The library comes in two variants. The default variant embeds a WebAssembly build of zlib, while the "native" variant embeds a JavaScript port of zlib instead. This embedded code is used when compressing data with a non-default level, when decompressing Deflate64 entries, when the useCompressionStream option is set to false, or when the Compression Streams API is unavailable. Otherwise, both variants rely on the Compression Streams API and behave identically.

zip.js uses WebAssembly by default for more flexibility: the embedded zlib module works inside a web worker or in the main thread, and the same binary is shared between both contexts without duplicating any code. This also makes the default variant smaller. The "native" variant does not depend on WebAssembly, e.g. for React Native or pages with a Content Security Policy which does not allow wasm-unsafe-eval. React Native resolves the main entry point of the package to the "native" variant automatically.

You can import the "native" variant:

The smaller entry points also have a "native" twin, e.g. @zip.js/zip.js/lib/zip-core-native.js instead of @zip.js/zip.js/lib/zip-core.js. The "native" variant is not available on JSR.

Custom web workers and compression engines

The createWorker option of configure() lets you create the web workers yourself, with the standard pattern below that bundlers like webpack and Vite detect statically to compile the worker script and its imports into a separate asset. It takes precedence over workerURI.

import { configure } from "@zip.js/zip.js";

configure({
  createWorker: () => new Worker(new URL("./zip-worker.js", import.meta.url), { type: "module" })
});

A custom worker script calls initWorker(), exposed by the @zip.js/zip.js/worker entry point. It can register an alternative deflate implementation: classes implementing the CompressionStream and DecompressionStream interfaces with the "deflate-raw" format, e.g. based on fflate. See the complete example in the API documentation of initWorker().

import { initWorker } from "@zip.js/zip.js/worker";
import { CompressionStreamFallback, DecompressionStreamFallback } from "./fflate-streams.js";

initWorker({ CompressionStreamFallback, DecompressionStreamFallback });

The @zip.js/zip.js/lib/zip-core-custom.js entry point is designed for this use case: it offers the full API but embeds neither the web worker code nor any deflate implementation. Combined with createWorker, the compression engine of your choice then ships only once, in the worker script. In a test with fflate on version 2.20.0, the application bundle and the worker script weighed approximately 140KB in total, 49KB gzipped, versus approximately 203KB, 86KB gzipped, for the default entry point.

import { configure } from "@zip.js/zip.js/lib/zip-core-custom.js";

configure({
  createWorker: () => new Worker(new URL("./zip-worker.js", import.meta.url), { type: "module" })
});

The same classes can also be registered in the main thread via the CompressionStreamFallback and DecompressionStreamFallback options of configure(), e.g. when useWebWorkers is set to false. Like the embedded implementation they replace, they are used when useCompressionStream is set to false or when the Compression Streams API is unavailable. These options were previously named CompressionStreamZlib and DecompressionStreamZlib, the old names are deprecated but remain supported. The registered implementation also determines the support of custom compression level values and, via the "deflate64-raw" format, of Deflate64 decompression.

Zstandard entries

Zip files with entries compressed with Zstandard (compression method 93) are produced by 7-Zip, WinZip and, since Node.js 26.8, by the ZIP API of node:zlib. zip.js lists these entries but reports an ERR_UNSUPPORTED_COMPRESSION error when reading their data until a codec is registered with registerCodec(). A codec is a pair of classes implementing the CompressionStream and DecompressionStream interfaces; the versionNeeded value 63 is the version required to extract Zstandard entries.

In Node.js 22.15 and later, node:zlib provides Zstandard streams, so the codec below needs no dependency. It reads and writes such entries with ZipReader and ZipWriter, the written entries are readable by 7-Zip and by the ZIP API of node:zlib, and the uncompressedSize option received by the compression class is passed on as the pledged source size when it is known.

import { Duplex } from "node:stream";
import { createZstdCompress, createZstdDecompress } from "node:zlib";
import { registerCodec, ZipWriter } from "@zip.js/zip.js";

class ZstdCompressionStream {
  constructor(format, options) {
    return Duplex.toWeb(createZstdCompress({ pledgedSrcSize: options.uncompressedSize }));
  }
}

class ZstdDecompressionStream {
  constructor() {
    return Duplex.toWeb(createZstdDecompress());
  }
}

registerCodec({
  compressionMethod: 93,
  format: "zstd",
  versionNeeded: 63,
  CompressionStream: ZstdCompressionStream,
  DecompressionStream: ZstdDecompressionStream
});

const zipWriter = new ZipWriter(writer);
await zipWriter.add("lorem.txt", reader, { compressionMethod: 93 });

Bun implements the proposal adding the format to the Compression Streams API, so its classes can be registered as they are: zip.js constructs them with the registered format as first argument.

registerCodec({
  compressionMethod: 93,
  format: "zstd",
  versionNeeded: 63,
  CompressionStream,
  DecompressionStream
});

In browsers and Deno, no Zstandard implementation is built in yet. Reading is covered by a JavaScript decoder such as fzstd, wrapped in a TransformStream as in the codec used by the test suite; writing needs a WebAssembly build of Zstandard. When a codec is registered with the codecURI option instead of classes, it runs in the web workers.

Build

The online builder generates a customized standalone build of zip.js directly in the browser, from the latest version published on npm. You can restrict the API to reading or writing zip files, include the web workers, choose the embedded compression implementation (WebAssembly, JavaScript, or none to rely on the Compression Streams API only), include the filesystem API, and download the result as a minified UMD or ES module file.

You can also build the library from the source code:

$ git clone https://github.com/gildas-lormeau/zip.js.git
$ cd zip.js
$ npm ci
$ npm run build

The bundles are generated in the /dist directory. npm test runs the test suite with Node.js, Deno and Bun, and npm run test-browsers runs it in Chrome, Firefox and Safari. See the /tests directory for more details.