A JavaScript library to zip and unzip files in the browser, Deno and Node.js
Last release 2 days ago
25 Aug 2026
Release timing varies
gaps range from 8 days to 3 months
Nearly every release is documented
notes for 60 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
353 releases · first in 2021
Release timeline
353 releases since 2021Releases
- 2.8.6025 Aug 2026
Release notes
Open source →What's Changed in v2.8.60
New features
- New
VERSIONconstant exposing the version of the library at runtime (e.g."2.8.60"). It matches theversiondeclared inpackage.json; the continuous integration verifies the agreement - New
getRegisteredCodecs()function. It returns the definitions of the codecs registered withregisterCodec(), as snapshots that cannot alter the registry. TheCompressionStreamandDecompressionStreamclasses of a codec registered withcodecURIappear in the result once its module has been imported - New
getSupportedCompressionMethods()function. It returns the compression methods supported in the current environment and configuration: the built-in methods resolved against the compression streams available at the time of the call, followed by the registered codecs. Each entry reports thecompressionanddecompressionsupport separately, e.g. Deflate64 is read-only. The support of a codec registered withcodecURIonly is reported asundefineduntil its module is imported - Registered compression codecs now receive the size of the source data as
CompressionStreamOptions#uncompressedSizewhen the reader has a known size. Codecs such as Zstandard can use it as the pledged source size and include the content size in the compressed frame (#675)
Bug fixes
- The
zip-fs-corebuild now exports the full core API. It previously exported only the filesystem classes, soconfigure(),registerCodec(), the reader and writer classes, and the constants were unreachable from this build
Documentation
- The
Readerclass documents how to implement random access to files opened with the runtime APIs, with a Deno example - The
offsetandusdzoptions are documented as read when theZipWriteris created and ignored when passed toZipWriter#add, and the default value ofoffsetread fromWriter#sizeis documented
Tests and continuous integration
- New tests cover the registered codec snapshots, the codec constructor options transmitted to web workers, the
VERSIONconstant, and the supported compression methods including the deferred resolution ofcodecURIcodecs - The version bump now rebuilds the bundles so the published files embed the version, and the continuous integration verifies that the version constant agrees with the declared versions
Credits
- New
- 2.8.5924 Aug 2026
Release notes
Open source →What's Changed in v2.8.59
New features
- New
ZipReader#warningsproperty andwarningsproperty on entries. They report non-fatal anomalies noticed while reading, as an array of{ reason, filename? }objects deduplicated by reason.ZipReader#warningsis replaced on eachgetEntries()call and collects the archive-level observations: an unsorted central directory, an unknown "version needed to extract", the compressed patched data bit, a malformed extra field, unknown zip64 extensible data, and a wrapped 16-bit entry count. The entry-levelwarningsproperty is set bygetData()and collects the local file header observations. The checks controlled by thestrictnessoption deposit a warning with the same reason when a lower strictness tolerates what "strict" rejects: appended or prepended data, trailing central directory data, duplicate filenames, a mismatched zip64 end of central directory record, and local file header mismatches. The warnings only report bytes the parse already read, so enabling nothing costs no additional I/O. The reasons are exported as 14WARNING_*constants - New
isZipFile()function. It returnstrueif the data looks like a zip file, i.e. ifZipReader#getEntriescalled on the same data would locate the archive structure. It runs the same end-anchored search asZipReaderand verifies that a central directory record is stored where the end of central directory record points, without parsing the entries. ThestrictnessandmaxAppendedDataSizeoptions control the tolerated appended data with the same semantics and defaults asZipReader - New
centralExtraFieldoption ofZipWriter#add. It sets an extra field written only in the central directory record, complementing thelocalExtraFieldoption which targets the local file header and theextraFieldoption which targets both
Behavior changes
- Leading and trailing whitespace in entry names is now preserved by
ZipWriter#addinstead of being silently trimmed. The zip specification does not restrict whitespace in filenames; note that Windows filesystems cannot represent a trailing space or dot in a name - Unclaimed bytes lying between the last central directory record and the end of central directory record are now detected, even when the declared central directory size matches the records. The "strict" strictness rejects such archives with the
ERR_AMBIGUOUS_ARCHIVEerror and the lower strictness levels deposit the "trailing central directory data" warning. These bytes were previously accepted silently at every strictness level, although the gap can hide records that other readers interpret, e.g. an unadvertised zip64 end of central directory record, and Info-ZIP and 7-Zip both flag such files. The check is skipped when the central directory is encrypted, because the plaintext is legitimately shorter than the stored data
Tests and continuous integration
- A new test suite covers the warnings: each reason is triggered by byte surgery on a well-formed zip file and asserted both as a warning at the tolerant levels and as a rejection at the levels that make the corresponding check throw
- New regression tests lock the preserved whitespace in entry names, the
isZipFile()probe, thecentralExtraFieldoption, and the detection of unclaimed bytes before the end of central directory record
Credits
- Claude (Fable 5) contributed to every change listed above
- New
- 2.8.5824 Aug 2026
Release notes
Open source →What's Changed in v2.8.58
New features
- New
ZipWriter#appendZipmethod. It copies the entries of an existing zip file into the current zip. UnlikeprependZip, it can be called at any position: after entries have been added, betweenadd()calls, and repeatedly to merge several zip files. The central directory of the copied file is rebuilt and its entries are relocated to the positions they get in the output.prependZipis kept as a deprecated alias - New
rawLastModDateoption ofZipWriter#add. It sets the raw MS-DOS date and time of the entry directly, whichpassThroughcopies of ZipCrypto entries need (see below) - New
localDirectory.dataOffsetproperty. It is the byte offset of the entry data, i.e. the entry offset plus the size of the local file header, of the filename and of the extra field. It can be used withReader#createReadableto serve ranged requests into an entry stored without compression - New
ERR_ZIP_CRYPTO_LAST_MOD_DATEerror constant
Behavior changes
- Errors of
add()andappendZip()calls left un-awaited are not lost anymore.close()waits for the pending calls and throws the first unreported error, with all of them available in itsentryErrorsproperty. Throwing counts as reporting: catching the error and callingclose()again finalizes the zip file without the failed entries.ZipWriterStreamnow aborts its writable when an entry fails, so the readable errors instead of hanging - An interrupted
appendZip()copy now setshasCorruptedEntrieson the writer and keeps the offsets of the entries written after it consistent - The "version needed to extract" field is now 10 for entries stored without compression or encryption, instead of 20
- Last modification dates before 1980 are now clamped to the MS-DOS epoch instead of underflowing the date field
- The
directoryproperty of read entries is now derived from the trailing slash of the filename alone. A name ending with "/" is a folder even when the entry declares an uncompressed size - Unicode Path and Unicode Comment extra fields are now applied only when their version is 1, as required by section 4.6.8 of the zip specification
- Reading an archive with a multiple of 65,536 entries and no zip64 record now returns all the entries. The 16-bit count of the end of central directory record wraps around; the reader detects the wrap by walking the central directory records past the declared count. The recovery is skipped when the strictness checks reject ambiguous archives
- The
unsafe*optimizations of the minifier were removed from the builds. Two of them shipped real miscompilations in the past, one of which stayed undetected for four years, and the size they saved was about 50 bytes per compressed bundle
Bug fixes
- ZipCrypto entries copied with
passThroughcan now be read back with their password. The password verification byte of ZipCrypto depends on the raw date of the entry when a data descriptor is used, so a copy that regenerated the date or forced the descriptor failed withERR_INVALID_PASSWORD. ThedataDescriptoroption is not forced anymore for pass-through ZipCrypto data, the newrawLastModDateoption preserves the raw date, and the filesystem API forwards both when exporting, throwing the newERR_ZIP_CRYPTO_LAST_MOD_DATEerror if the date is overridden - The end of central directory records of split zip files now declare the number of central directory entries stored on the last disk, as required by section 4.4.21 of the zip specification, instead of the total. The count is 0 when the record starts on a fresh disk, which is how Info-ZIP fills the field. The check deciding whether the disk number of the record requires zip64 also accounts for the actual record and comment length when predicting a disk rollover near 65,535 disks
- The spanning signature of split zip files is now written while holding the writer lock. A first entry written with
bufferedWrite, or interleaved un-awaitedadd()calls, could fail on the locked stream or misplace the signature - Duplicate filenames are now detected before the entry waits for a worker slot. Two
add()calls with the same filename made while the worker pool was saturated could both be accepted
Documentation
- The
usdzoption states that its constraints apply to the entries written withadd()only. The entries copied withappendZipkeep the layout of the source zip file and are not checked - The
ZipReaderconstructor states that a stream input is buffered entirely in memory, because reading a zip file requires random access, and points at customReaderimplementations for large seekable resources WritableWriter#sizestates that a value set before the first write is used as the starting offsetuseUnicodeFileNamesstates that disabling it only clears the language encoding flag and does not re-encode the filenamespassThroughdocuments the coupling between ZipCrypto and the last modification datemsDosCompatibledocuments how PKUNZIP handles folder entries
Tests and continuous integration
- New PKZIP 1.10 and PKZIP 2.04g fixtures (attributes, comments, spanned archives) with tests reading the archives produced by the original tools
- New regression tests lock the fixes above: the spanning signature position with concurrent and buffered writes, the per-disk entry counts of spanning central directories, including in the golden output, the ZipCrypto pass-through copies, the queued duplicate names, and the retryable
close() - The Safari suite retries up to four times, the ZipCrypto tests are immune to date and wrong-password flakes, and the test fixtures are resolved independently of the working directory
- The dead extra field handling of the
appendZipentry rebuild was removed, making it explicit that copied entries carry their extra fields verbatim
- New
- 2.8.5722 Aug 2026
Release notes
Open source →What's Changed in v2.8.57
New features
- New
ERR_UNSUPPORTED_UINT64error constant
Behavior changes
- Folders and empty stored entries are now written without a data descriptor. Their checksum and sizes are zero and known before the data is written, so bit 3 of the general purpose bit flag and the descriptor declared nothing, and other writers, e.g. Info-ZIP, leave them out. The local file header now carries the zeroed values directly, which shrinks every folder by the length of the descriptor. Those entries are also written directly instead of going through the buffered write path, since there is nothing to buffer. The descriptor is still written when the
dataDescriptoroption is set explicitly, and for encrypted entries - Reading an archive that declares a 64-bit value above
Number.MAX_SAFE_INTEGERnow throws the newERR_UNSUPPORTED_UINT64error. JavaScript numbers lose integer precision above 2^53 - 1, so a size or an offset that large was silently rounded to a nearby value and every computation derived from it was wrong. No valid archive is affected, such values describe contents beyond 8 PB
Bug fixes
- The central directory records rebuilt by
ZipWriter#prependZipnow keep the zip64 layout of the source entries. The zip64 fields were selected again from the sizes of each entry, so a record whose source stored, e.g., only one of its sizes in the zip64 extra field was rebuilt with a different layout, and the zip64 field left in the raw extra field of the entry could be written twice. Prepending an archive and adding entries now produces the same bytes as writing all the entries directly - The InfoZip Unix extra field now stores both ids when only one of the
uidandgidoptions is set, the missing one defaults to 0. The field used to declare the missing id with a length of 0, a layout Info-ZIP never writes and readers are not required to accept
Documentation
unixExtraFieldTypenow states that the filesystem API re-emits the uid and gid of imported entries as "infozip" whatever the field type found in the imported zip file, unless the option is set explicitly
Tests and continuous integration
- The browser testing workflow was improved by @danny0838: the browser driver is determined automatically, the download URL of old Chromium versions is computed from the version, and the runner works on Windows (#674)
- The web test runner now waits for the cleanup of the test frames before removing them and reports the in-flight tests when a run fails, and the old Chromium versions of the matrix are pinned to their snapshot position
- The worker backpressure test now registers a codec that never signals backpressure and measures a deterministic bound on every runtime, instead of relying on the margin left by the native codec of each engine
- New regression tests lock the fixes and changes above: the data descriptor rules for folders and empty stored entries, including the golden output, the zip64 layout of prepended entries, and the layout of the InfoZip Unix extra field when a single id is set
- A new test locks that
ZipWriter#removereturnsfalsefor an entry whoseadd()is still in flight and that the entry is written normally
- New
- 2.8.5622 Aug 2026
Release notes
Open source →What's Changed in v2.8.56
Bug fixes
- The compression method stored in the WinZip AES extra field is no longer truncated to its low byte. The field declares the actual compression method of an encrypted entry in a 16-bit slot, and it was written with a single byte store. An encrypted entry using a method above 255, which a codec registered with
registerCodeccan use, announced a different method, so readers selected the wrong codec after decrypting. Registered codecs exist since v2.8.37 - Writing an entry with the
usdzoption now throwsERR_INVALID_EXTRAFIELD_DATAwhen the extra field could exceed 64KB once the alignment padding is added. The padding is computed after the length check and adds up to 67 bytes, so an extra field close to the limit wrapped the 16-bit length field and produced a corrupt entry - The central directory written by
ZipWriter#prependZipnow points at the prepended entries when the writer has an initial offset. The offsets were computed from the source archive alone, so an archive written with theoffsetoption, or into a writer already holding data, declared offsets short by that initial offset and the prepended entries could not be read back. The two features could be combined since v2.7.71 - The digital signature record is now written within a single segment of a split archive. It could start at the end of one segment and continue in the next, unlike every other record, because only the end of central directory record checked the remaining space before being written. The writer now closes the disk first for the signature record as well. Signing is available since v2.8.47
ZipReader#digitalSignatureis now defined on a split archive whose central directory starts on an earlier disk than the end of central directory record. The reader looked for the record in the bytes read for the central directory, which stop at the declared directory length in that case, so the signature was never found. The record is now read from the file when it does not follow the directory in those bytesZipDirectoryEntry#getExportedSizenow throwsERR_UNDETERMINED_SIZEwhen the predicted size depends on the order the entries are written. The zip64 fields of an entry depend on its offset, so when the entries total more than 4GB and the write order is not guaranteed, e.g.keepOrderset tofalse, two orders can produce two sizes. The detection walked the entries in enumeration order, so it missed layouts where only another order crosses the threshold, and the exported archive could differ from the prediction. The prediction is available since v2.8.52ZipDirectoryEntry#getExportedSizenow honors theoffsetoption. The option shifts the offsets stored in the central directory, which select the zip64 fields, and the prediction computed them from zero. Predicting an export withoffsetat 4GB or above returned a size short by the zip64 records the export actually writes
Documentation
CodecDefinition#codecURInow states that bundlers and single-file builds, e.g.deno compile, cannot follow the dynamic import of the codec module, so the module must be included explicitly, withdeno compile --includeor the equivalent option of the bundler
Tests and continuous integration
- Every fix above is locked by a regression test: an encrypted round trip with a registered method above 255, a
usdzentry with an extra field near the 64KB limit,prependZipinto a writer with an initial offset, a sweep of segment sizes checking the placement and the read back of the digital signature record on split archives, and size predictions with order-dependent zip64 layouts and with theoffsetoption
- The compression method stored in the WinZip AES extra field is no longer truncated to its low byte. The field declares the actual compression method of an encrypted entry in a 16-bit slot, and it was written with a single byte store. An encrypted entry using a method above 255, which a codec registered with
- 2.8.5521 Aug 2026
Release notes
Open source →What's Changed in v2.8.55
New features
- New
ERR_UNDEFINED_COMPRESSION_METHODerror constant
Behavior changes
The two
passThroughrules below change what the option accepts. Code that copies entries between archives by passing the compression method of the source entry, which is what the filesystem API does, is unaffected.- Writing an entry with the
passThroughoption now requires thecompressionMethodoption, and throws the newERR_UNDEFINED_COMPRESSION_METHODwhen it is missing. The data is copied as-is, so that option selects no codec, it declares how the data is already compressed and is written into the headers of the entry verbatim. It used to fall back to Deflate whatever the data was, so copying a stored entry without setting it produced an archive announcing Deflate over stored bytes, which no reader can decompress. Copying an entry read withZipReaderis a matter of passing itscompressionMethodalong. The entries with no content, e.g. the directories, ignore the option, as they ignorepassThroughitself - The
leveloption is now ignored for the entries written withpassThrough. The data is never compressed, so the option describes nothing, and yetlevelset to 0 used to select the compression method written in the headers, stored instead of Deflate, and any level used to set the level bits of the general purpose bit flag. SetcompressionMethodto declare how the data is compressed.levelkeeps applying to the other entries of the same archive, so exporting a filesystem withlevelset andpassThroughset in the reader options still compresses the entries that were added to it and copies the entries that came from a zip file - The
decryptCentralDirectorycallback now receives the encrypted central directory alone. It used to be given the whole declared range of the directory, which also holds the digital signature record when the archive is signed, so the callback was handed bytes it cannot decrypt. The length is taken from the encryption header of the zip64 end of central directory record when it declares one, and falls back to the declared length of the directory. The callback has been given the whole range since it was introduced in v2.8.47
Bug fixes
- Reading an archive whose central directory is encrypted no longer moves the directory somewhere else. The reader checks that the offset declared in the end of central directory record points at a central file header, and an encrypted directory carries no such signature, so the check failed and the offset was reconciled to another position. The offset is now trusted as well when the encryption header declares the size of the encrypted directory, and when the bytes at the offset look like an encrypted directory
- The extensible data sector of the zip64 end of central directory record is now counted in the length of that record. The sector holds the encryption header of an encrypted central directory, and the reader subtracted the fixed length of the record only, so the computed end of the central directory sat past its real end by the length of the sector, and every offset derived from it was wrong
ZipReader#digitalSignatureis now defined on an archive whose central directory is encrypted. The digital signature record follows the encrypted directory, so it is not part of what the decryption returns, and the reader looked for it in the decrypted bytes only- The local file headers masked by PKWARE strong encryption no longer make
FileEntry#getData()throwERR_AMBIGUOUS_ARCHIVE. When the central directory is encrypted, the local file header of an entry carries a placeholder filename and a zeroed checksum and sizes, and says so with bit 13 of its general purpose bit flag. The comparison against the central directory record that became the default in v2.8.53 rejected those archives. The filename, the checksum and the sizes are now left out of the comparison for those entries, the general purpose bit flag and the compression method are still compared - The published bundles no longer set the level bits of the general purpose bit flag on the entries written with the default compression level.
index.min.jsand the files ofdist/announced "super fast" on every Deflate entry whoseleveloption was left unset, where the sources announce nothing. The minifier was configured withunsafe_comps, enabled in February 2022, which rewrites a comparison into its negation:!(0 > level)is true for an undefined level wherelevel >= 0is false. Those bits are advisory and no reader decompresses differently because of them, but an archive written by a bundle differed from the same archive written from the sources. The option is dropped from both minifier configurations, which costs 40 bytes onindex.min.js
Documentation
ZipReader#digitalSignaturenow describes what the signature covers: the records of the central directory, read atZipReader#directoryOffset, never including the digital signature record itself. zip.js does not verify signaturesZipReader#directoryLengthnow warns that some writers, e.g. SecureZIP, count the digital signature record in the length they declare, so verifying the whole declared range can never succeed. Subtract6 + digitalSignature.lengthfrom it when the record is stored inside the declared range- The
passThroughoption now describes howlevelandcompressionMethodare treated, and states that the compression method is written into the headers as-is instead of selecting a codec ZipReaderStreamnow states that it reads its input entirely into aBlobbefore it emits the first entry, since a zip file stores its central directory at the end. It is a convenience wrapper aroundZipReaderfor stream sources, it does not extract the entries while the data is still arriving
Tests and continuous integration
- New fixtures written by SecureZIP cover the strong encryption formats: AES-128, AES-192 and AES-256, stored and Deflate64 entries, an encrypted central directory, a certificate-based archive, a signed archive and an archive mixing encrypted and clear entries. They are read by the tests of
decryptCentralDirectoryand by the SecureZIP archive tests - A new fixture written by a third-party tool covers a zip64 entry whose sizes are stored in a data descriptor
- The zip64 fixture of the HTTP test is rebuilt with real zip64 records, the previous one carried none although the test was named after them
- The writer backpressure test now registers a codec that never signals backpressure on its writable side, instead of forcing the native one. It measured the invariant on Bun only, where the native
CompressionStreambehaved that way, and stopped measuring anything once Bun 1.4.0 fixed it. The registered codec keeps the test meaningful on every runtime - A new test locks the two
passThroughrules above: the compression method is required, the level is ignored, and the level keeps applying to the other entries of the archive
- New
- 2.8.5420 Aug 2026
Release notes
Open source →What's Changed in v2.8.54
Breaking changes
ZipWriter#prependZip()now reads an array of readers as the disks of a split zip file, which is what an array denotes everywhere else in the API, and accepts aSplitDataReaderinstance the same way. The disks are read in order and the entries are relocated to the positions they get in the output. An array used to be concatenated and read as a single archive, which produced wrong offsets for a real split zip file. If you were passing an array of byte ranges of one zip file, concatenate them yourself and pass a single readerZipEntry#moveTo()is removed. It was deprecated and undeclared in the TypeScript definitions, and was a one-line alias ofZipFS#move(), which is the method to use- The undeclared
ZipFS#addData()andZipDirectoryEntry#addData()methods are removed. They were internal, never documented and never declared. The typedaddText(),addBlob(),addUint8Array(),addData64URI(),addHttpContent(),addReadable(),addFile(),addFileSystemEntry()andaddFileSystemHandle()methods cover what they did
Security
Both fixes below are reachable from an untrusted zip file read with
ZipReader. Upgrading is recommended for anyone reading archives they did not produce.FileEntry#getData()now throws the newERR_ENTRY_DATA_OUT_OF_BOUNDSwhen the declared data of an entry, i.e. its offset plus its compressed size, ends past the end of the zip file. Such an entry used to makegetData()hang for ever, with no error and no CPU use, so nothing timed out and nothing showed up in a profile. Honestly truncated archives are affected as much as malformed ones. A read past the end of the source now ends the stream instead of stalling it, which also covers the entries the bounds check cannot detect in advance- The output of
FileEntry#getData()is no longer allocated from the declared uncompressed size of the entry. The size was reserved before a byte was read, so a 131-byte archive declaring 3 GiB reserved 3 GiB. The allocation is clamped to what the compressed data can decode to,compressedSize * 1032for a compressed entry andcompressedSizefor a stored one, 1032 being the maximum expansion ratio of Deflate. The clamp never binds on real data, a legitimate archive still preallocates exactly its uncompressed size
New features
ZipFS,ZipEntry,ZipFileEntryandZipDirectoryEntryare now exported at the top level, and thefsnamespace is deprecated. Replacenew zip.fs.FS()withnew zip.ZipFS(), andzip.fs.ZipFileEntrywithzip.ZipFileEntry.zip.fskeeps working and the library emits no runtime warning, the deprecation is documentation only.ZipEntryis now a value as well, soentry instanceof zip.ZipEntryworks. The three entry classes were already declared as top-level exports but existed at runtime underzip.fs.*only, so importing them type-checked and then failed. In TypeScript, theFStype is deprecated and kept as an alias ofZipFS, solet fs: FSkeeps compiling- New
ZipEntry#setOptions()method andZipEntry#optionsproperty in the filesystem API.setOptions()merges the options into the ones the entry was added with, an option set toundefinedbeing removed instead of stored, and they are applied when the zip file is exported. It is the way to set the options of an entry imported from a zip file, which has none until it is called. The options describing the data of an entry exported withpassThrough, e.g.compressionMethodanduncompressedSize, are ignored, they are always the ones of the original entry, and so aredirectoryand the progress callbacks ZipWriter#prependZip()now writes a correct split zip file when the writer is a split zip file writer. The whole prepended archive used to be copied into the first disk, so every entry recorded an offset on the wrong disk. The data is copied disk by disk now, a disk is closed before an entry whose local file header would not fit in what is left of it, and each entry records the disk it starts on and its offset in that disk. The output also starts with the split zip file signature, unless the prepended zip file already carries oneTextWriternow decodes CP437.new TextWriter("cp437")used to return the data decoded as UTF-8, since the encoding was handed toFileReader#readAsText(), which falls back to UTF-8 for a label it does not know. It goes through the same decoder as the filenames and the comments now. It also decodes withTextDecoderinstead ofFileReader, which removes the last dependency on that class, missing from some worker scopes. The byte order mark is still removed, whichever branch decodes the data- The second argument of the codec stream constructors is now typed, by the exported
CompressionStreamOptionsandDecompressionStreamOptionsinterfaces. They document which members are set for which class, e.g.deflate64only for the deflate implementations, andrawBitFlag,compressionMethodanduncompressedSizeonly for the codecs registered withregisterCodec().Configuration#CompressionStream,Configuration#DecompressionStream, their*Fallbackand deprecated*Zlibforms andCodecDefinitionare declared with them instead of the untypedTransformStreamLike. This only concerns you if you pass a custom stream implementation or callregisterCodec() Configuration#baseURIis now declared. It resolves the relativeworkerURI,wasmURIandcodecURIvalues, and defaults to the URL of the module of zip.jsWritableWriter#sizeis now declared. zip.js sets it to 0 before the first write and keeps it updated, so a customWritercan read how many bytes have been written so far, e.g. to compute the offset of a disk. It is declared onWriter,TextWriter,BlobWriter,SplitDataWriterandUint8ArrayWriteras well- Members that existed and were not declared:
HttpReader#url,TextWriter#encoding,BlobWriter#contentType,Data64URIWriter#contentType,EntryError#overlappingEntryandEntryError#reason.overlappingEntryis the only way to identify the other entry of the pair reported byERR_OVERLAPPING_ENTRY, andreasondescribes the ambiguity reported byERR_AMBIGUOUS_ARCHIVE
Behavior changes
The options listed first used to accept values of the wrong type and produced a wrong, empty or silently dropped result. They throw now. If your code passes the documented types, nothing changes.
lastModDate,lastAccessDateandcreationDatemust beDateinstances and throw the newERR_INVALID_DATEotherwise. An invalidDateused to be written as an entry carrying no timestamp at all. A timestamp expressed in milliseconds is the natural mistake and is rejected as well: passnew Date(file.lastModified), notfile.lastModified- The
commentoption of an entry must be a string and throws the newERR_INVALID_ENTRY_COMMENT_TYPEotherwise. AUint8Arrayused to be coerced and its textual representation written into the archive. Decode the bytes to a string before passing them - The
extraFieldoption must be aMap, and throws the newERR_INVALID_EXTRAFIELDotherwise. Its keys must be integers between 0 and 65535, andERR_INVALID_EXTRAFIELD_TYPEnow covers a non-integer or a negative key as well as a key above 65535. Its values must beUint8Arrayinstances, and throw the newERR_INVALID_EXTRAFIELD_DATA_TYPEotherwise - The
readerOptionsoption ofZipDirectoryEntry#export*(),ZipDirectoryEntry#getExportedSize()andZipDirectoryEntry#exportFileSystemHandle()must be an object and throws the newERR_INVALID_READER_OPTIONSotherwise. A value of another type was silently ignored: a password passed as a string instead of an object failed with the unrelatedERR_ENCRYPTED, while the other options were dropped without any error. An unknown property of areaderOptionsobject is still ignored, as everywhere else in the API - The options expecting a function throw the new
ERR_INVALID_FUNCTION_OPTIONwhen they are given a value of another type:encodeText,decodeText,createTempStream,signCentralDirectoryanddecryptCentralDirectory. A falsy value keeps meaning "use the default" - The
signaloption throws the newERR_INVALID_SIGNALwhen it does not look like anAbortSignal, i.e. when it does not expose anaddEventListener()method and a booleanabortedproperty. Duck-typed signals and signals coming from another realm keep working - The
passwordandrawPasswordoptions are now checked on the reader side as well, throwingERR_INVALID_PASSWORD_TYPE. A value of another type used to fail with the unrelatedERR_ENCRYPTEDorERR_INVALID_PASSWORD msdosAttributesRawthrowsERR_INVALID_MSDOS_ATTRIBUTESwhen the value is not an integer, and accepts a numeric string like the other numeric options. The range check used to be the only one, and the bitwise arithmetic folding the value into the external file attributes did the rest quietly: a fractional value was truncated, and a value that is not a number at all passed both comparisons and was written as 0.msdosAttributesthrowsERR_INVALID_MSDOS_DATAon an array, which used to be accepted as an object and wrote 0 as well, since none of the flag properties exist on itconfigure()andsetDefaultConfiguration()reject two kinds of bad input instead of storing them.maxWorkersmust be an integer greater than 0 and throws the newERR_INVALID_MAX_WORKERSotherwise: a value lower than 1 used to deadlockZipWriter#add()for ever, since no entry could start and none could release the next one.createWorkerand theCompressionStreamandDecompressionStreamoptions, including their*Fallbackand deprecated*Zlibforms, must be functions and throwERR_INVALID_FUNCTION_OPTIONotherwise, a falsy value still meaning "use the default". The numeric options accept a numeric string and are coerced, like the numeric options of the reader and the writer. Nothing is stored unless the whole call passes, so a rejected call leaves the configuration untouchedchunkSizeis normalized wherever it is read. A value lower than 64 is raised to 64, as before, and a value that is not an integer greater than 0 now falls back to the default of 65536 instead of being used as it is. This applies to the global configuration and to thechunkSizeoption ofReader#createReadable()
The rest of this section changes results rather than rejecting input.
- The configuration is read when it is used instead of when the reader or the writer is constructed. A
configure()call made betweennew ZipWriter()and the firstadd(), or betweennew ZipReader()and the firstgetData(), used to be ignored and is honored now. It affectsmaxWorkers,chunkSize, the compression stream implementations and the deflate support detection - The
executableoption now counts as Unix metadata, likeunixMode. It means a mode of0o755, and it was the only Unix metadata option taking the MS-DOS branch, so an entry written withexecutableset next tomsDosCompatible,msdosAttributesormsdosAttributesRawlost its executable bit without a word. It wins over the three of them now, and selects the Unix platform for the "Version made by" field.executableset tofalsechanges nothing, as before - The entries of the filesystem API are dated when they are added, not when they are written. An entry added without a
lastModDateoption used to be stamped with the current date at export time, so exporting an unchanged tree twice produced different bytes. The four sources of the date of an exported entry now rank as follows, weakest first: the moment the entry was added, the date of the entry the tree was imported from, thelastModDateoption passed to the export, and thelastModDateoption passed when the entry was added - The directories implied by the name of an imported entry are no longer written back when the tree is exported. Importing a zip file storing
"a/b.txt"and no directory entry creates a navigable"a"entry, which used to be exported as an entry of its own, so a round trip gained one entry per path component. Only the directories carried by the source zip file and the ones created withaddDirectory()are written now.getExportedSize()and the progress callbacks count them the same way - The options passed when an entry was added no longer override the values describing the data of an entry exported with
passThrough.compressionMethod,uncompressedSizeand the other pass-through values now win over the per-entry options, as they already did over the options passed to the export - The options passed when an entry is added are now copied. The object was stored as it was, so mutating it afterwards, or reusing one object for several entries and mutating it in between, changed entries that had already been added
- The split zip file signature at the start of a zip file is no longer reported as prepended data. A single-disk archive written by
SplitDataWriterstarts with the four bytesPK\x07\x08, which used to be read as prepended data byextractPrependedDataand rejected as an ambiguous archive bystrictnessset to"strict"or bycheckAmbiguity. The temporary spanning markerPK00, which PKZIP writes at the start of the first disk while a spanned archive is being created, is accepted the same way. If you read archives produced bySplitDataWriterwith either of those options, they are no longer misreported
Bug fixes
ZipWriter#prependZip()no longer copies the central directory of the zip file it prepends. The whole source was piped into the output, then the entries were rewritten after it, so every prepended archive carried its old central directory as dead bytes in the middle of the result. Only the data region is copied now, i.e. everything before the first byte of the central directory. Archives already produced this way are still readable, they are only larger than they need to beZipWriter#prependZip()reads the zip file it prepends once instead of twice. A reader providingreadUint8Array()was buffered into aBlobbecause the central directory was read throughreader.readable, so aBlobReaderor anHttpRangeReaderwas fully downloaded into memory before anything was written- The transferred streams are no longer broken in a web worker installing a polyfill of the Streams API, the setup documented under
Configuration#createWorkerbelow. The worker received a nativeReadableStreamand a nativeWritableStreamthroughpostMessage()and piped them into streams of the polyfill, which rejected them. They go through the same compatibility wrappers as the streams created in the worker now
Documentation
msdosAttributesRawandmsdosAttributesnow describe the platform they select. The behavior is unchanged: setting either of them selects the MS-DOS platform for the entry exactly asmsDosCompatibleset totruedoes, and overrides that option when it is explicitly set tofalse, soversionMadeByloses its Unix upper byte and no Unix mode is written. What counts is that the option is provided, not its value, so0and{}select it too. Any Unix metadata option wins over the three of them, with the MS-DOS attributes written into the low byte, see theexecutablechange aboveZipWriter#prependZip()now states that the data of the zip file is copied, its central directory rebuilt and its entries relocated, so the disks of a split zip file passed as input are unrelated to the disks of the outputZipReader#commentand thecommentoption of an entry now explain why one is bytes and the other a string. The encoding of the comment of an entry is recorded in its header by the general purpose bit 11, the encoding of the global comment is recorded nowhere, so it can only be decoded with the encoding agreed with the producer of the zip fileConfiguration#createWorkernow documents how to install a polyfill of the Streams API in the scope of the worker, which is the way to run the web workers on the engines whereTransformStreamis missing from that scope, e.g. Firefox before version 102. A polyfill imported by the page does not help, because the worker reads the globals of the Streams API from its own scopeConfiguration#workerURInow states that the worker is created as a module worker, unless the URI is a Data URI or a Blob URI, in which case it is created as a classic workerZipDirectoryEntryExportOptionsnow documents the precedence of the four sources of the last modification date of an exported entry, andZipDirectoryEntry#importZip()documents that the directories implied by an entry name are not written backFile#lastModifiedis now a link to MDN in the generated documentation, instead of an unresolved reference
Tests and continuous integration
- A new audit checks that every property name reaching the public API is either declared in
index.d.tsor mangled on purpose. It reads the terser configuration and the declarations, walks the objects the library builds at runtime and reports the names belonging to neither list. It found eight internal fields shipped under their source names in the minified builds, e.g. the pending characters ofData64URIWriterand the source blob ofBlobReader, which are mangled now, two of them renamed on the way. The parameter names of the declarations are no longer reserved either: a parameter name is not a property name, and reserving it kept a field of the same name readable in every build - The internal methods writing the records of a zip file are renamed away from the DOM property names they shared, so that they are mangled instead of being kept by the terser reserved list
- Every public class is instantiated by the audit, so a member appearing only on an instance is covered. The classes reached only through a subclass, e.g.
ZipEntry, are matched by walking the prototype chain - The deprecated
checkSignatureoption is covered by a test of its own, and the rest of the suite usescheckCrc32. The option had lost all its usages when the tests were swept, so nothing exercised it any more - The
zip.fsnamespace is covered by a test of its own, for the same reason - The polyfill of the Streams API in the worker is tested with a classic worker, a module worker and the native build, one test per build since the WASM worker and the native worker bundle different codecs. The web runner gained a
nativeBuildfeature probe and caches the build probe instead of running it per test - New tests: the entries whose declared data extends past the end of the archive, the options of an entry of the filesystem API, the pass-through values against the per-entry options, the stability of the dates of an exported tree, the directories implied by an imported entry name, the encoding of
TextWriter, the single read ofprependZip(), the bytes it copies, its split zip file output, and the strictness of the split zip file signature - An option validation assertion is split into a test file of its own, since it needs the
signaloption ofpipeTo(), which Chrome 76 to 79 ignore. The runner skips the file there rather than reporting a failure - The release is now scripted.
npm run bump-patchbumps the version inpackage.json,package-lock.jsonanddeno.jsonand commits it, the version script of npm syncingdeno.json. A workflow step verifies that the three declared versions agree, and the release workflow verifies that the released tag matches the version declared inpackage.json
Credits
- Andrew Chin, Brian J Lee and Youngjoon Kim, SSLab at Georgia Tech, reported the declared uncompressed size of an entry driving the allocation of the output
- Claude (Opus 5) contributed to every change listed above
Full Changelog: v2.8.53...v2.8.54
- 2.8.5319 Aug 2026
Release notes
Open source →What's Changed in v2.8.53
New features
- New
checkLocalDirectoryoption in the reader options. It compares the local file header of an entry against its central directory record whenFileEntry#getData()is called, and throwsERR_AMBIGUOUS_ARCHIVEwhen the two disagree.truecompares the filename, the general purpose bit flag, the compression method, the CRC-32 checksum and the sizes, likestrictnessset to"strict";falsecompares nothing, like"tolerant". Setting it explicitly always wins overstrictness, whetherstrictnesswas passed to the constructor ofZipReaderor to the call, so it is the way to ask for this one check without the archive-level checks ofcheckAmbiguity, and the way to drop it without giving up the other checksstrictnessperforms. It is also the only way to validate the local file headers of a self-extracting archive, sincecheckAmbiguityrejects prepended data outright - The local file header of an entry now reports the two records the reader had already read and dropped.
LocalDirectory#rawFilenameholds the filename stored in the local file header, which is allowed to differ fromEntryMetaData#rawFilename, and is defined whenstrictnessis"strict"orcheckLocalDirectoryistrue.LocalDirectory#dataDescriptorholds the data descriptor record written after the content, described by the newLocalDataDescriptorinterface, and is defined whencheckOverlappingEntryorcheckOverlappingEntryOnlyis set. It carries the CRC-32 checksum and the sizes stored in the record, each of which is allowed to differ from the central directory, and asignatureflag telling whether the record is preceded by its optional signature. That signature is not part of the original format, it is a later convention writers are free to follow. When the four bytes look like the signature but the values behind them disagree with the central directory, the flag isfalseand the record is read as starting at those four bytes instead - The parsed extra field records are now typed instead of being declared as the bare
EntryExtraField.EntryExtraFieldZip64,EntryExtraFieldNTFS,EntryExtraFieldExtendedTimestampandEntryExtraFieldUnixdescribe the members the reader fills in,EntryExtraFieldUnicodegainsversion,filenameandcomment, andEntryExtraFieldAESgainscompressionMethod, the real compression method of the entry, next tooriginalCompressionMethod, which is the99a WinZip AES header is required to carry in its place SplitDataReadernow accepts an array ofReaderinstances, ofReadableReaderinstances or ofReadableStreaminstances. The last two were declared in the TypeScript definitions and worked nowhere: reading a split archive requires the size of every disk to map a global offset onto one of them, so an element that only provides a stream is now buffered when the reader is initialized. This applies wherever an array of readers is accepted, i.e. the constructor ofZipReader,ZipWriter#add(),ZipWriter#prependZip(),ZipDirectoryEntry#importZip()and thereaderproperty of aZipFileEntryinstanceZipWriter#prependZip()now accepts a reader that only provides aReadableStream. It reads the central directory of the archive it prepends before piping it, so passing a stream used to fail withTypeError: ReadableStream is already locked. The stream is buffered once, like the disks above- New
ERR_INVALID_COMMENT_TYPEerror constant
Behavior changes
- The local file header of an entry is now compared against its central directory record by default, except for the filename.
strictnessset to"balanced", the default, used to trust the central directory record entirely;getData()now throwsERR_AMBIGUOUS_ARCHIVEwhen the general purpose bit flag, the compression method, the CRC-32 checksum or the sizes disagree.getEntries()is unaffected, the local file header is only read when the data is. This costs nothing: every one of those fields is read from the local file header anyway to locate the entry data. Only the filename is left out, because comparing it reads the filename bytes as well, which costs one extra read per entry whenever the local file header carries no extra field, the common case."strict"still compares the filename too,"tolerant"still compares nothing, andcheckLocalDirectoryset tofalserestores the previous behavior. The new default was verified against 458,000 entries of real archives, where it rejects none of them - An explicit
checkAmbiguitynow wins over an inheritedstrictness.checkAmbiguityis the boolean form ofstrictness,truemeaning"strict", and the two used to be resolved without regard to where they came from, so acheckAmbiguitypassed togetEntries()or togetData()could not relax astrictnesspassed to the constructor ofZipReader. A value passed to the call now wins over a value passed to the constructor, andstrictnessstill wins overcheckAmbiguitywhen both are passed to the same one.checkAmbiguityset tofalsemeans "not strict" rather than "trust everything", so it downgrades an inherited"strict"to"balanced"and leaves an inherited"tolerant"alone; passstrictnessset to"tolerant"to compare nothing. Code that passesstrictnessand nevercheckAmbiguityresolves exactly as before - The platform byte of the "Version made by" field is now forced instead of being merged into the value given by the
versionMadeByoption. It is set to Unix (3) when the entry carries Unix metadata, i.e. whenuid,gid,unixModeorunixExtraFieldTypeis set, and to MS-DOS (0) whenmsdosAttributesormsdosAttributesRawis set. Only the lower byte of the given value survives in both cases. It used to be combined with the byte already present, so aversionMadeBycarrying another platform produced a value belonging to neither ZipWriter#close()now throws the newERR_INVALID_COMMENT_TYPEerror when the comment it is given is not aUint8Array. Passing a string, the natural mistake, used to fail deep inside the writer withTypeError: Cannot read properties of undefined (reading 'byteLength'), after the entries had been written.getExportedSize()performs the same check on theglobalCommentoption
Bug fixes
- The Unix user and group ids are now read from the local file header when the central directory has none. The Info-ZIP Unix type 2 extra field (0x7855) stores them in the local file header only and leaves a zero-length copy in the central directory, so
uidandgidwere undefined on every archive written by Info-ZIP. They are filled in when the data of the entry is read: they are still undefined aftergetEntries()and appear oncegetData()has run, since that is when the local file header is read, and they are also readable onEntryMetaData#localDirectory. A value read from the central directory is never overwritten by the local file header, since the type 2 field truncates the ids to 16 bits while the New Unix field (0x7875) does not - An empty Info-ZIP Unix type 2 extra field no longer hides the ids of the Info-ZIP New Unix extra field next to it. The reader looked at 0x7875 only when 0x7855 was absent, so an entry carrying both, which is what Info-ZIP writes, reported no ids at all although 0x7875 held them
EntryMetaData#rawLastAccessDateandEntryMetaData#rawCreationDateare now filled from the NTFS extra field. They were declared but never set: the rawFILETIMEvalues were stored on the extra field record only.EntryMetaData#rawLastModDateis unaffected, it remains the MS-DOS date and time stored in the header- The entries returned by
ZipReader#getEntries()now carryrawBitFlag,filenameLength,extraFieldLengthandunixExternalUpper. The four properties were declared onEntryMetaDataand read from the central directory, they were simply dropped when the entry object was built unixExternalUpperis now the upper half of theexternalFileAttributesthe entry was written with, on the entry returned byZipWriter#add(). It was computed before theunixModeoption and the Unix file type were folded in, so it reported the default0o644for every entry, whatever the mode: an entry written with0o120777disagreed both with its ownexternalFileAttributesand with what the reader reports for it- A worker that fails to load now falls back to the main scope instead of throwing a
TypeError. The codec pool builds the worker and its interface, then calls it back one turn later; when theerrorevent of the worker arrived in that interval, the error was dropped and the pool went on to post a message to a worker it had already discarded, which failed withCannot read properties of null (reading 'postMessage'). The designed fallback now runs in that case too, with the error of the worker as the reason. This affects the engines where a worker cannot be started at all, e.g. Firefox extensions using manifest v2 and Chromium 76 to 79 - The entry returned by
ZipWriter#add()now defines the same members as the entries returned byZipReader#getEntries():zip64,symlink,encrypted,zipCryptoandmsDosCompatiblewere left undefined instead offalseon one side or the other, and the deprecatedinternalFileAttributeandexternalFileAttributealiases were missing from it
Documentation
- The
strictnessoption now lists the fields each level compares, and states which of them are read from the local file header anyway - The
versionMadeBy,msDosCompatibleandunixModeoptions now describe how the platform byte and the Unix file type are chosen, including the fact that a folder entry is always written withS_IFDIRwhatever type the mode carries - The
symlinkproperty now points at the option that writes a symbolic link, since there is no option of that name: the file type goes inunixMode, i.e.0o120777with the path of the target as the content of the entry - Several documented defaults disagreed with the code and were corrected:
maxWorkersfalls back to 2 when the environment provides nonavigator.hardwareConcurrency,workerURIpoints at the worker of the build that was imported, the fourCompressionStreamandDecompressionStreamoptions default to the global implementations or to the one embedded in the entry point,lastAccessDateandcreationDatehave no default at all so that the entries do not carry a meaningless time, andversionMadeBydefaults to 768 rather than 20 preventHeadRequestnow states that leaving it unset is not the same as setting it tofalsewhenuseRangeHeaderorforceRangeRequestsis set: the size is then read from a rangedGETrequest, and only an explicitfalserestores theHEADrequestrawLastModDatenow states that it is the MS-DOS date and time of the header and is not replaced by the value of the NTFS extra field, unlikelastModDate
Tests and continuous integration
- A workflow step rebuilds the project on every push and fails when the committed build output differs. The release workflow publishes the committed files as they are, it never rebuilds, so a stale
dist/orindex.min.jsat a tag would ship to npm and JSR. Pull requests are exempt, asking outside contributors to commit build output would add an unreviewable diff to every change - The release workflow now publishes only when the test suite passed on the released commit
- A new audit compares the shapes of the objects the library builds at runtime against the interfaces declared in
index.d.ts, in addition to the audit of the read and write surfaces added in v2.8.52. It is what found the members left undefined and the extra field records declared as the bareEntryExtraField - A regression test covers the backpressure of the writer on the web worker path
- The browser runner restarts the browser and runs the suite again when the session is lost, and its
--headfuloption was renamed to--headed - The Safari job runs on macOS 15 instead of macOS latest, where the browser loses its window or its session in the middle of the suite more often
- The workers are terminated between the tests in the Node.js, Deno and Bun runners, and the Bun runner sets its own timeout
- A test reproduces the ordering that made a failing worker throw instead of falling back, i.e. the error of the worker arriving before the pool sends it its first message
Credits
- Claude (Opus 5) contributed to every change listed above
Full Changelog: v2.8.52...v2.8.53
- New
- 2.8.5218 Aug 2026
Release notes
Open source →What's Changed in v2.8.52
New features
- New
getExportedSize()method onZipDirectoryEntryandFSinstances. It returns the exact size of the zip file the matchingexport*()call would write, without writing it. It takes the same options as theexport*()methods, so the value it returns is the one the export produces. It is meant for theContent-Lengthheader of a streamed download. It throws the newERR_UNDETERMINED_SIZEerror when the size cannot be known before writing, i.e. when an entry is compressed, when an entry has no known size, whensignCentralDirectoryis set, and when thebufferedWriteoption lets the physical layout depend on the order in which the entries are written. Encryption does not prevent the prediction, its overhead is a fixed number of bytes - New
onentryprogressoption in theexport*()methods of the filesystem API. It is called once per written entry with the number of entries written, the total number of entries, and the entry itself. It reports the entries whereasonprogressreports the bytes. It is called after the entry has been written. WhenbufferedWriteis enabled the entries are written concurrently, so it counts the entries written instead of giving the position of the entry in the zip file - New
globalCommentoption in theexport*()methods of the filesystem API. It sets the comment of the zip file. The options of these methods are applied to every entry, so settingcommentthere comments each entry instead of the archive, exactly aslastModDatethere sets the date of each entry. The zip file comment therefore needed a name of its own importZip()now accepts aZipReaderinstance in addition to the data of a zip file. The caller builds the reader, passes it, and keeps it after the import. This is the way to readprependedData,appendedData,comment,digitalSignature,directoryOffsetanddirectoryLength, which are only filled once the entries have been read. The options of the reader are merged with the options of the import, and the options of the import win- New
symlinkproperty on entries. It istruewhen the entry is a symbolic link. The target of the link is the content of the entry, which is read like any other entry, e.g. withgetData(new TextWriter()). The target is not validated, it can be an absolute path or escape the directory of the entry, so it must be checked before being used. Writing a symbolic link is done by setting theunixModeoption to a mode carrying theS_IFLNKtype, e.g.0o120777, with the path of the target as content exportFileSystemHandle()now accepts thereaderOptionsoption. Thepasswordoption must be set there to export the entries of an encrypted zip file, since thepasswordoption of the export encrypts the written entries instead- The
readerOptionsoption of the filesystem export now acceptspassThrough. The entries imported from a zip file are then written as-is, without being decompressed and decrypted, exactly as importing them with this option does. The entries added to the filesystem are compressed as usual - The
signCentralDirectoryoption is now declared in the export options of the filesystem API. It was already forwarded to theZipWriterinstance, it was simply missing from the TypeScript definitions - New
ERR_UNDETERMINED_SIZE,ERR_INVALID_PASS_THROUGHandERR_UNSUPPORTED_ENCRYPTION_PASS_THROUGHerror constants
Behavior changes
- The
export*()methods of the filesystem API now report the progress of the whole archive instead of the progress of each entry.onstartandonendare called once, with the total size of the entries and with the number of bytes written. They used to be called once per entry, whileonprogresswas already reporting the archive as a whole, so the three callbacks disagreed with each other.onprogressis unchanged. Use the newonentryprogressoption to be notified for each entry - The export options of the filesystem API now take precedence over the metadata of the entries imported from a zip file. Setting
lastModDate,comment,versionMadeBy,uid,gidor the file attributes in the export options used to have no effect on those entries, althoughunixModeandmsdosAttributesdid reach them, so the same attribute word was writable one way and not the other. The order is now the metadata of the source entry, then the export options, then the description of the entries copied as-is, then the options of the entry. The description of the entries copied as-is stays above the export options because it describes the bytes being copied, not an intent. Alevelor acompressionMethodwinning over it would write headers that do not match the content. An export using the default options is unchanged, so round trips keep their fidelity ZipReader#close()now cancels theReadableStreaminstance passed to the constructor when nothing has been read from it. It used to do nothing at all. The stream of a reader whose entries have been read is left alone, and so is aReaderinstance, which belongs to the caller. The entries stay readable after the call- The
preventCloseoption is now honored only when the caller owns the writable, i.e. when aWritableWriterinstance is passed toexportZip()orexportWritable(). It is ignored by the otherexport*()methods of the filesystem API, whose Writer instance can only return its data once its writable is closed. Setting it there used to prevent the export from ever resolving - Writing an entry with the
passThroughoption and a password now throwsERR_UNSUPPORTED_ENCRYPTION_PASS_THROUGH. The data is copied as-is, so it cannot be encrypted. The password used to be ignored silently and the entry was written unencrypted although its header announced encryption - The
passThroughoption is now ignored for the entries with no content.add("dir/", undefined, { directory: true, passThrough: true })used to throwERR_UNDEFINED_READER, so aZipWriterinstance created withpassThroughset totruecould not write a single directory - Writing an entry with the
unixModeoption now stamps the type of the file in the external file attributes. zip.js used to write a mode with no type, e.g.0o000644where Info-ZIP writes0o100644, sounzip -llisted the entry as?rw-r--r--. A mode already carrying a type is left untouched, and theexternalFileAttributesoption still writes the value verbatim - The
executableproperty of an entry is nowfalsefor symbolic links. The permission bits of a link are always0o777, so the flag was meaningless on every link
Bug fixes
- Reading a range of data no longer emits empty chunks.
Reader#createReadable()enqueued an empty chunk at the end of every entry whose size is known, and one extra read was issued for an entry of unknown size. The data was correct, the stream simply contained a chunk of length 0 getBlob()andgetData64URI()now honor the MIME type they are given. A filesystem entry holding aBlobinstance returned it as-is, with the MIME type it was created with, and ignored the requested one- The filesystem API now runs the overlapping entry check when
checkOverlappingEntryOnlyis set in its reader options. The option means "run the check and stop before reading the content" in the core API. The filesystem API needs the content, so it used to drop the option and skip the check altogether. It is now mapped tocheckOverlappingEntry, so the check runs and the content is still read - The reader options that withhold the content of an entry are now ignored by the filesystem API.
checkPasswordOnly,checkOverlappingEntryOnlyandpreventCloseused to be forwarded to the entries read from a zip file, which returned no data - Progress is now reported for the filesystem entries that were not imported from a zip file. Reading such an entry pipes the reader to the writer without going through a codec, so
onprogressnever fired. It is now reported for every entry exportFileSystemHandle()now counts the bytes it writes. It used to report the compressed size of each entry against the uncompressed size of the archive, so the progress stopped around 1% of the total, and it reported nothing at all for the entries that were not imported from a zip fileaddFileSystemHandle()now applies the entry options to the directories it creates. They were applied to the files only- The dates of the Info-ZIP unix extra field are now read as signed timestamps. A date before 1970 was read as a date in 2106
- Writing an entry whose name ends with a slash no longer corrupts the
unixModeoption. The type of the file was combined with the type already present in the mode instead of replacing it, so0o100644became a socket and0o120777became an invalid type. Both were then read back as neither a directory nor a symbolic link - The deprecated
externalFileAttributeandinternalFileAttributeoptions work again. They were removed when they were renamed toexternalFileAttributesandinternalFileAttributes. They are back as deprecated aliases, and an option passed toadd()now takes precedence over the option of theZipWriterinstance whatever the spelling of each addText()now stores the size of the text in bytes instead of its number of UTF-16 code units. OnlygetExportedSize()read that value, so the size was under-reported for any text containing non-ASCII characters. The export itself was correct- The buffer reserved for the expansion of the deflate algorithm is now allocated only for the entries that are really compressed. A stored entry reserved the same margin as a deflated one
Performance
- The
export*()methods of the filesystem API now keep a running total instead of summing the progress of every entry on every tick. The cost was quadratic in the number of entries. Passingonprogresson an archive of 10,000 entries added 50% to the duration of the export, and now adds nothing measurable
Documentation
- The
useCompressionStreamoption now states that the native API is used for compression only whenlevelis undefined or equal to 6.CompressionStreamdoes not support compression levels, so any other value compresses the data with the embedded implementation. It also states that the data produced at a given level can vary between platforms, and thatuseCompressionStreammust be set tofalseto get the same output everywhere - The
uidandgidoptions now state which unix extra field carries them ZipReader#close()and thecreateTempStreamoption now describe what they do. A temporary stream must be able to hold a whole entry, because the local header written before it holds the size and the CRC-32 of the entry. Its readable side is therefore consumed only once its writable side has been closed. A factory returningnew TransformStream()deadlocks, whereas the default buffers everything
Tests and continuous integration
- The browser test runner uses Selenium instead of Playwright. It accepts
--exe-path,--url-search,--build,--headfuland--help, and thetest-webkitscript was renamed totest-safari - A workflow runs the test suite on every push, including the native build
- A workflow checks that the markdown documentation is up to date with
index.d.ts - The API documentation is published to the GitHub Pages site on each release
- The web runner accepts a
maxParallelTestsparameter. Chromium 87 loses the wake-up of the backpressure of a stream when 16 tests run in parallel, so its jobs cap the parallelism at 4 - The tests that check the abort reason are skipped on the browsers that ignore the signal of
pipeTo()instead of being reported as failures
Credits
- @danny0838 contributed the switch to Selenium, the test workflow and the arguments of the browser runner (#673)
- Claude (Opus 5) contributed to every change listed above
Full Changelog: v2.8.51...v2.8.52
- New
- 2.8.5115 Aug 2026
Release notes
Open source →What's Changed in v2.8.51
New features
getChildren()returns the children of a directory as an array, and all its descendants when therecursiveoption is set totrue. It is available onZipDirectoryEntryandFSinstances. The descendants are ordered level by level, like the result ofreaddir(path, { recursive: true })in Node.js, which is also the order in which the entries are written by theexport*()methods. Unlike theentriesproperty ofFS, the array excludes the root directory, leaves no empty slot for removed entries, and can start from any directory. It is a snapshot taken when the method is called, so the tree can be modified while the array is being iterated
Behavior changes
- The
export*()methods of the filesystem API now write the entries in the same order whatever the value of thebufferedWriteoption. Setting it tofalseused to write each branch of the tree entirely before moving to the next one, whereas the default writes the entries level by level, so an archive exported withbufferedWriteset tofalsedoes not have the same entry order as in the previous versions. Only the order changes: the entries, their content and their metadata are identical, and a directory entry still precedes the entries it contains
Compatibility improvements
exportFileSystemHandle()called withconcurrentset totruenow reports the failure that stopped the export on browsers which do not support thereasonargument ofAbortController#abort(), e.g. Firefox 79 and Chromium 87. zip.js used to recognize the cancellation of the sibling entries by the reason it had passed toabort(). These browsers discard that reason and report a plainAbortErrorinstead, so the cancellation was reported as the cause of the failure and the original error was demoted intoentryErrors. The cancellation is now tracked by zip.js itself and never read back from the platform (see #669)exportFileSystemHandle()now rejects with an error instead of rejecting withundefinedwhen the export is aborted through thesignaloption on these browsers. The reason passed toAbortController#abort()is discarded by the platform and cannot be recovered, so aDOMExceptionnamedAbortErroris thrown in its place. Its message is exposed as the newERR_ABORTEDconstant. Testingerror.name == "AbortError"now identifies an aborted export on every supported platform, whereas these browsers used to report a plainErrorwhen the export was aborted before it started and anAbortErrorwhen it was aborted while an entry was streaming
Documentation
- The API documentation of
exportFileSystemHandle()now states that an entry flagged as a symbolic link is written as a regular file whose content is the path of the link target, since the File System Access API cannot create symbolic links
Tests
- The test verifying that the abort reason of the caller is forwarded is now skipped on browsers without support for
AbortSignal#reasoninstead of being reported as a failure. It moved to its own file and covers aborting before the export as well as aborting while an entry is streaming
Credits
- @danny0838 reported the failure on Firefox 79 and Chromium 87 and ran the test suite on these browsers
Full Changelog: v2.8.50...v2.8.51
- 2.8.5014 Aug 2026
Release notes
Open source →What's Changed in v2.8.50
New features
- New
filenameValidationoption inZipReaderandgetEntries(). It rejects entry names that do not map safely to a file path. It accepts"strict","balanced"and"tolerant", and defaults to the value of thestrictnessoption. Rejected names throw the newERR_UNSAFE_FILENAMEerror, which carries the offending name in itsfilenameproperty - New
normalizeFilenameoption inZipReaderandgetEntries(). It is called with the decoded name of each entry and its result replaces that name. It runs after decoding and before validation, so repairing a name is enough to make it passfilenameValidation. Returningundefinedkeeps the decoded name. The filesystem API inherits the option fromZipReaderConstructorOptions - The
decodeTextandencodeTextoptions now receive the type of the text they handle,"filename"or"comment", as their last argument. Hooks declaring fewer parameters keep working exportFileSystemHandle()now reports what happened when an export fails. The newentryNameproperty ofEntryErrorholds the name of the entry that failed, relative to the exported entry. The newexportedEntryNamesproperty lists the files that were completely written before the failure. Every other file of the export is either missing or empty, so this is the only way to tell a file the export completed from one it created but never filledexportFileSystemHandle()called withconcurrentset totruenow collects every failure instead of reporting only the first one. The other failures are listed in the newentryErrorsproperty ofEntryError. Failures raised deeper in the tree are flattened into that list- New
ERR_INVALID_LEVEL,ERR_INVALID_PASSWORD_TYPE,ERR_INVALID_STRICTNESS,ERR_INVALID_FILENAME_VALIDATIONandERR_INVALID_MAX_APPENDED_DATA_SIZEerror constants
Breaking changes
- Entry names containing a
..path component, or starting with/, with a drive letter likeC:, or with\\, are now rejected when reading an archive. They throwERR_UNSAFE_FILENAME. SetfilenameValidationto"tolerant"to restore the previous behavior. This default was verified against a corpus of 572 archives holding 257,333 entries. None of them was rejected, so the new default costs nothing on real archives. A backslash is never treated as a path separator. It is legal on UNIX file systems, and it also occurs as the trail byte of double-byte filenames in Shift-JIS, Big5 and GBK, where converting it would corrupt the name - The
strictnessoption set to"strict"now also rejects empty and.path components, e.g.a//b.txtand./cur.txt - Invalid option values now throw instead of falling back silently to a default. A
leveloutside the integers 0 to 9 throwsERR_INVALID_LEVEL, and used to disable compression entirely when it was negative or not a number. Apasswordthat is not a string, or arawPasswordthat is not aUint8Array, throwsERR_INVALID_PASSWORD_TYPE. A value of another type used to produce an unencrypted archive, and arawPasswordpassed as a string used to produce an archive that its equivalentpasswordcannot open. An unknownstrictnessorfilenameValidationthrows instead of behaving as"balanced". An invalidmaxAppendedDataSizethrows instead of being accepted. A non-integerencryptionStrength,uid,gidorunixModeis now rejected by the guard whose message already announced it. Values meaning "no password", i.e.undefined,null, an empty string and an emptyUint8Array, keep working as before. Numeric options also keep accepting the strings that represent them, e.g.levelset to"9", because form controls, query strings and environment variables all yield strings - The filesystem API now ignores empty and
.path components when importing an archive. Names such asa//b.txtand./cur.txtno longer create entries with an empty name or named. - When reading an entry fails, the
writableof the writer is now aborted with the error instead of being closed. A custom writer used to observe a successful close although the data was truncated
Bug fixes
- Errors raised by
addFileSystemHandle()andexportFileSystemHandle()are now rethrown unmodified instead of being wrapped. Theirmessageis comparable to the exportedERR_*constants again, and theirentryNameproperty identifies the handle or the entry that failed exportFileSystemHandle()called withconcurrentset totruenow cancels the entries that have not started yet when an entry fails, instead of letting the rest of the export run to completion
Build and packaging
- Web worker support is now tree-shakable. The web worker backend was moved to a separate module which registers itself when imported. Custom builds that do not import it no longer include the web worker plumbing and run codecs inline. The standard entry points import it, so the published builds are unaffected
- The most frequently used globals are destructured in the intro of the bundles, which reduces the size of the minified builds
Full Changelog: v2.8.49...v2.8.50
- New
- 2.8.4913 Aug 2026
Release notes
Open source →What's Changed in v2.8.49
Compatibility improvements
- The embedded JavaScript deflate engine is now compiled with an ES2019 target. It no longer contains nullish coalescing operators, which require Chromium 80. As a result, the WebAssembly and "native" builds now work down to Chromium 76 instead of 80 (see #669). The compatibility table on https://gildas-lormeau.github.io/zip.js/ has been updated accordingly
- Fixed a regression introduced in v2.8.18: when the worker script passed via
workerURIcould not be started as a module worker, zip.js fell back silently to inline workers instead of retrying with a classic worker. Browsers without module worker support, e.g. Chromium 76 to 79, now load external worker scripts again - Codecs registered with
registerCodec()and acodecURInow run inline transparently when the web worker cannot import the codec module, e.g. on browsers which do not supportimport()in workers like Chromium 76 to 79 and Firefox 102 to 113. SettinguseWebWorkerstofalseis no longer needed on these browsers
These changes were verified by running the test suite on real Chromium 76, 79 and 80 builds. Known limitation on Chromium 76 to 79: aborting operations in progress has no effect because these versions ignore the
signaloption ofpipeTo(). The data read or written remains correct.Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.48...v2.8.49
- 2.8.4813 Aug 2026
Release notes
Open source →Bug fixes
- Fixed reading and writing data when the global stream classes are polyfilled, e.g. with web-streams-polyfill on Firefox 79. Reading a zip file from a stream,
Entry#arrayBuffer(),createBlobTempStream()and the filesystem API could fail or hang because zip.js consumed streams internally with the nativeResponseconstructor orReadableStream#pipeTo(), which do not accept polyfilled streams. With the polyfill loaded, the whole test suite now passes in an environment withoutTransformStream. Thanks to @danny0838 for running the test suite on older browsers (#669) - Fixed the generation of temporary file names in
createOPFSTempStream()andcreateSyncAccessHandleTempStream()whencrypto.randomUUIDis unavailable, e.g. in Chrome 67 to 91 or Safari 14.1 to 15.3
Other changes
- Browser tests requiring a feature the browser does not support, e.g.
CompressionStreamor OPFS, are now reported as skipped instead of failing, which makes the test results meaningful on older browsers - New
npm run serve-testsscript to run the tests manually in a browser, and a new README documenting how to run and write tests
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.47...v2.8.48
- Fixed reading and writing data when the global stream classes are polyfilled, e.g. with web-streams-polyfill on Firefox 79. Reading a zip file from a stream,
- 2.8.4713 Aug 2026
Release notes
Open source →What's Changed in v2.8.47
New features
- New
signCentralDirectoryoption inZipWriter#close()to sign zip files. The function receives the raw data of the central directory records and returns the data of a digital signature record, e.g. a PKCS#7 signature, written between the central directory and the end of central directory record. The signature is exposed by the newdigitalSignatureproperty ofZipReader, along with the newdirectoryOffsetanddirectoryLengthproperties to locate the signed data and verify it. zip.js stores the data as-is and does not implement the signature computation itself - New
decryptCentralDirectoryoption inZipReaderto read zip files with an encrypted central directory, as defined in the Strong Encryption Specification of the ZIP format specification. The function receives the raw data stored in place of the central directory and the encryption metadata exposed in aDirectoryEncryptionInfoobject, and returns the decrypted central directory records. Without this option, reading such zip files throws anERR_ENCRYPTED_CENTRAL_DIRECTORYerror. zip.js provides the encrypted data and the related metadata but does not implement the decryption itself - All the user-facing error message constants are now exported, e.g.
ERR_INVALID_UID,ERR_INVALID_MSDOS_ATTRIBUTES,ERR_ENTRY_EXISTS,ERR_UNSUPPORTED_CRYPTO_APIandERR_WORKER_STARTUP_TIMEOUT. This allows comparingerror.messagewith the constants instead of hardcoding the messages
Other changes
- Improved browser testing, thanks to @danny0838 (#672): module import errors are now reported in the test results, and the new
withStreamsPolyfillURL parameter runs the test suite with web-streams-polyfill on browsers missingTransformStream
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.46...v2.8.47
- New
- 2.8.4613 Aug 2026
Release notes
Open source →What's Changed in v2.8.46
New features
- New
createWorkeroption inconfigure()to create the web workers, taking precedence overworkerURI. It enables the standard bundler patternnew Worker(new URL("./zip-worker.js", import.meta.url), { type: "module" }), letting bundlers like webpack and Vite detect the worker script statically and compile it with its imports into a separate asset - New
initWorker()function exposed by the new@zip.js/zip.js/workerentry point to write custom worker scripts. It can register alternative implementations ofCompressionStream/DecompressionStreamused to compress/decompress data, e.g. based on fflate, and aninithook called with the configuration, e.g. to load a WebAssembly module. The API reference ofinitWorker()includes a complete example based on fflate. Existing custom worker scripts relying on theinitModuleconvention keep working - New
@zip.js/zip.js/lib/zip-core-custom.jsentry point offering the full API without embedding any web worker code or deflate implementation. Combined withcreateWorkerand a custom worker script, the compression engine of your choice ships only once, in the worker script - New
CompressionStreamFallbackandDecompressionStreamFallbackoptions inconfigure()replacing the deprecatedCompressionStreamZlibandDecompressionStreamZliboptions. The new names reflect the role of these implementations: the fallback used whenuseCompressionStreamis set tofalseor when the Compression Streams API is unavailable
Other changes
- New section in the documentation covering custom web workers and compression engines: https://gildas-lormeau.github.io/zip.js/#custom-workers
- The files embedding the web worker code and the WebAssembly module are now generated with rollup plugins instead of placeholder replacements, and the development and release build configurations have been merged
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.45...v2.8.46
- New
- 2.8.4513 Aug 2026
Release notes
Open source →What's Changed in v2.8.45
New features
- New
localExtraFieldoption inZipWriter#add()to write extra field records only in the local file header, complementing theextraFieldoption which writes them in both the local file header and the central directory. This helps reproducing entries whose records appear only in the local file header, as written by some tools - The Info-ZIP Unix type 1 extra field (0x5855), written notably by macOS Archive Utility and
ditto, and the PKWARE Unix extra field (0x000d) are now read. They are exposed via the newextraFieldUnixType1andextraFieldPkwareUnixproperties of entries, and populate thelastModDate,lastAccessDate,uidandgidproperties. The extended timestamp and NTFS extra fields still take precedence for the dates when present
Bug fixes
- AES-encrypted entries added with
passThroughset totrueand a providedcrc32value are now marked as AE-1 and store the CRC-32 in the entry headers, instead of being written as AE-2 entries with zeroed CRC-32 fields. Copying an AE-1 entry read with thepassThroughoption no longer loses the stored CRC-32. Without thecrc32option, entries are still written as AE-2 - The export methods of the filesystem API, e.g.
FS#exportBlob(), now forward the internal file attributes, theuid/gidvalues (written as an Info-ZIP Unix extra field, or as configured withunixExtraFieldType), and the custom extra field records of imported entries instead of dropping them - The
rawLastAccessDateandextraFieldUSDZproperties are now copied to the entries returned byZipReader#getEntries(); they were parsed but missing from the entry objects
Other changes
- The MIME type table is now generated at build time from a compact string encoding, reducing the builds embedding it by approximately 8.5KB when minified, e.g.
index.min.js, without changing the results ofgetMimeType() - The JSR package now only contains the files needed at runtime, shrinking it from 7.8MB (404 files) to 0.7MB (76 files), and all its entry points are now associated with the type declarations
- The browser test suites now run headlessly on Chrome, Firefox and WebKit with playwright. The WebKit suite adds Safari-engine coverage that did not exist before
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.44...v2.8.45
- New
- 2.8.4412 Aug 2026
Release notes
Open source →What's Changed in v2.8.44
New features
- New
checkCrc32option inZipReaderandgetData()replacing the deprecatedcheckSignatureoption. It also verifies the CRC-32 of entries encrypted with AES when the zip file uses the AE-1 format, which stores the real CRC-32 value - New
checkAuthenticationCodeoption, enabled by default, controlling the verification of the authentication code of entries encrypted with AES - New
crc32option replacing the deprecatedsignatureoption when adding entries withpassThroughset totrue - New
crc32property on entries replacing the deprecatedsignatureproperty. It isundefinedwhen the zip file does not store the CRC-32 value, e.g. for entries encrypted with AES in AE-2 format - New
ERR_INVALID_CRC32andERR_INVALID_AUTHENTICATION_CODEerror constants replacing the deprecatedERR_INVALID_SIGNATUREconstant. The CRC-32 verification throws the former, the verification of the authentication code of entries encrypted with AES throws the latter. The three constants share the same string value for backward compatibility, the two new constants will become distinct strings in the next minor version
Bug fixes
- Dates outside the range representable in the NTFS extra field are now clamped to its bounds, i.e. 1601-01-01 and the year 30828 (the maximum signed 64-bit
FILETIMEvalue, chosen because Windows and 7-Zip reject or misread values beyond it), instead of overflowing silently to an arbitrary value - The last modification date is now truncated to the whole second before rounding odd seconds up to the next even second in the DOS date. Sub-second fractions no longer round the DOS date up, aligning it with Info-ZIP and ditto and keeping it within 1 second of the extended timestamp (see #671)
- The deprecated
signatureproperty of entries encrypted with AES returned byZipWriter#add()is nowundefined, like the newcrc32property, instead of exposing the raw authentication code as aUint8Arraydespite being typed as anumber
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.43...v2.8.44
- New
- 2.8.4312 Aug 2026
Release notes
Open source →What's Changed in v2.8.43
New features
- The
gzipfallback introduced in v2.8.42 now supports reading AES-encrypted entries. Thegziptrailer is computed from the decompressed data instead of relying on the CRC-32 value stored in the central directory, which is zeroed for AES-encrypted entries. As a side effect, archives with a corrupted CRC-32 value can now be read with this fallback whencheckSignatureis not set, like with the other implementations - Entries are now stored uncompressed instead of failing when zip.js cannot compress data at all, e.g. when the Compression Streams API is unavailable and the WebAssembly module cannot load
Bug fixes
- Archives with an invalid uncompressed size in the central directory are now rejected with
ERR_INVALID_UNCOMPRESSED_SIZEwhen read with thegzipfallback, instead of failing with a misleading error
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.42...v2.8.43
- The
- 2.8.4212 Aug 2026
Release notes
Open source →What's Changed in v2.8.42
New features
- New entry point
@zip.js/zip.js/externalreferencing the web worker script and the WebAssembly module as external files instead of embedding them. Bundlers like webpack and Vite emitzip-web-worker.jsandzip-module.wasmas separate assets, which removes approximately 45KB of embedded payloads from the main bundle. The worker also runs from a real file URL. This avoidsblob:restrictions on pages and browser extensions with a strict Content Security Policy (cf. #669). Smaller compositions are also available:@zip.js/zip.js/lib/zip-fs-core-external.jsexcludes the MIME type table (approximately 23KB), and@zip.js/zip.js/lib/zip-core-external.jsalso excludes the filesystem API. The three compositions come with prebuilt ES module bundles in the/distdirectory, which resolve the two asset files from their own directory - New entry point
@zip.js/zip.js/mime-typesexposinggetMimeType()with the full MIME type table, usable from any build - zip.js now wraps raw deflate data in the
gzipformat and relies onCompressionStream("gzip")/DecompressionStream("gzip")when the"deflate-raw"format and the WebAssembly module are both unavailable. This makes reading and writing zip files work out of the box on Chromium 80 to 102 when a Content Security Policy blocks WebAssembly compilation, e.g. on extension pages (cf. #669), and lowers the minimum requirements of the "core" builds to Chrome 80, Node.js 18, and Deno 1.19. Reading AES-encrypted entries and Deflate64 entries is not supported with this fallback
Bug fixes
- A failure to load the WebAssembly module is now reported with an explicit "WASM module not loaded" error, with the original error as
cause, e.g. the CSP error, instead of aTypeErrorthrown later by the codec (cf. #669) - A failed load of the WebAssembly module is now retried on the next use instead of being cached for the whole session
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.41...v2.8.42
- New entry point
- 2.8.4111 Aug 2026
Release notes
Open source →What's Changed in v2.8.41
New features
- The
DecompressionStreamof codecs registered withregisterCodecnow receives thecompressionMethod,rawBitFlaganduncompressedSizevalues of the entry. TheCompressionStreamreceives thecompressionMethodvalue. This makes it possible to implement codecs for compression methods which cannot be decoded from the stream alone. For example, LZMA (method 14) without the end-of-stream marker, or the legacy Shrink, Reduce and Implode methods (1 to 6). Tests validate these changes with decompression codecs ported from hwzip for Shrink (method 1), Reduce (methods 2 to 5) and Implode (method 6), including Reduce archives produced by PKZIP 0.92. Tests also read DCL Implode archives (method 10) produced by SecureZIP with a codec based on node-pkware, and round-trip new entries with and without encryption
Bug fixes
- Streams passed to zip.js are now normalized when they come from another implementation of the Streams API. This fixes
TypeErrorexceptions and hangs when the global stream classes are overridden with a polyfill like web-streams-polyfill whileBlob#stream()andResponsestill return native streams, e.g. on Firefox 99 and older (cf. #669) - The dynamic import used to load codecs registered with a
codecURIvalue is now excluded from the static analysis of webpack and Vite. This fixes the "Critical dependency: the request of a dependency is an expression" warning when bundling zip.js (fix #670) - Entries protected with the strong encryption feature of PKWARE are now always rejected with
ERR_UNSUPPORTED_ENCRYPTION. They were previously rejected withERR_UNSUPPORTED_COMPRESSIONwhen the compression method was also unsupported
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.39...v2.8.41
- The
- 2.8.3911 Aug 2026
Release notes
Open source →What's Changed in v2.8.39
Bug fixes
- Reading split zip files and ZipCrypto-encrypted entries no longer depends on
Array.prototype.at, which was raising the minimum supported versions for these features to Chrome 92, Firefox 90 and Safari 15.4
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.38...v2.8.39
- Reading split zip files and ZipCrypto-encrypted entries no longer depends on
- 2.8.3811 Aug 2026
Release notes
Open source →What's Changed in v2.8.38
This release makes zip.js resilient to environments without
"deflate-raw"support in Compression Streams and to web workers failing silently (e.g. in browser extensions), detects archives with an encrypted central directory, and improves the fidelity of written zip files with other tools.New features
- New
workerStartupTimeoutoption inconfigure(): if a newly created web worker does not respond within this delay (5 seconds by default), it is terminated and the task runs inline transparently. This recovers from workers failing silently without any error event, e.g. worker scripts blocked by the Content Security Policy. Error events raised by workers which have never responded now also trigger the inline fallback instead of making the task fail. As part of this change, streams are only transferred to workers which have proven responsive, workers post a ready message as soon as their script is loaded, andterminateWorkers()re-enables the detection of web worker support - New
ERR_ENCRYPTED_CENTRAL_DIRECTORYerror:ZipReadernow detects archives with an encrypted central directory (PKWARE SES, e.g. produced by SecureZIP with filename encryption enabled) via the zip64 end of central directory record or the archive extra data record, and throws a clear error instead of failing with a misleading one - Entries encrypted with the strong encryption feature of PKWARE (bit 6 of the general purpose bit flag) are now rejected with
ERR_UNSUPPORTED_ENCRYPTIONinstead ofERR_INVALID_PASSWORDwhen reading
Bug fixes
- The automatic fallback on the embedded deflate/inflate implementation did not work when Compression Streams were unavailable or did not support the
"deflate-raw"format (e.g. Chromium <= 102, Firefox <= 112, older versions of Node.js), unlessuseCompressionStreamwas set tofalse(#669). The WebAssembly module is now initialized when needed, and its initialization is skipped for entries which do not need it (stored entries and entries using a custom codec) - Explicitly passed
externalFileAttributesvalues are now written verbatim, including0on directory entries. ThemsDosCompatibleand unix mode options (unixMode,setuid,setgid,sticky) now only override the bits they represent and preserve the other bits (bits 8 to 15 of the DOS attributes were previously dropped when a unix mode was recomposed)
Behavior changes
- Last modification dates with an odd number of seconds are now rounded up to the next even second in the DOS date/time fields instead of being truncated, consistently with Info-ZIP, 7-Zip, ditto and Windows. The extended timestamp and NTFS extra fields still store the exact date
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.37...v2.8.38
- New
- 2.8.3710 Aug 2026
Release notes
Open source →What's Changed in v2.8.37
This release adds support for third-party csutom compression codecs such as Zstandard, reduces the size of written zip files by storing the NTFS extra field only when necessary, and fixes several issues found by @danny0838.
New features
- New
registerCodec(definition)andunregisterCodec(compressionMethod)APIs to plug custom compression methods intoZipWriterandZipReader. ACodecDefinitionassociates a compression method ID and format name withCompressionStream/DecompressionStreamclasses, provided directly or lazy-loaded in workers viacodecURI, with an optionalversionNeeded. This enables reading and writing entries compressed with Zstandard (method 93), for example with the fzstd library. New errors:ERR_INVALID_CODEC_DEFINITION,ERR_RESERVED_COMPRESSION_METHOD,ERR_INVALID_CODEC_MODULE - New
ntfsTimestampoption (#666): the NTFS extra field is now written only when it preserves information the extended timestamp extra field cannot represent, i.e. a last modification date outside its supported range or explicitlastAccessDate/creationDatevalues. This saves 72 bytes per entry in the common case. Set it totrueto always write the field (previous behavior) orfalseto never write it
Bug fixes
- Compressing an empty file with a non-default
levelproduced an invalid entry (compressedSizeequal to 0) with the "native" variant using the JavaScript port of zlib (#667) - The maximum last modification date when
extendedTimestampis disabled is now clamped to 2107-12-31 23:59:58 instead of 2107-12-31 00:00:00, and the documentation no longer mentions the nonexistent date "November 31, 2107" (#665) - Entries with a malformed AES encryption strength (outside 1–3) are now rejected when reading instead of only when the strength is missing
- Trailing junk after the compressed data is now ignored when decompressing with the JavaScript port of zlib, consistently with the native implementations
Behavior changes
- The
usdzoption now stores entries uncompressed by default (unlesslevelorcompressionMethodis set explicitly) and throws the newERR_UNSUPPORTED_ENCRYPTION_USDZerror when a password is set, in accordance with the USDZ specification which mandates zero compression and no encryption
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.36...v2.8.37
- New
- 2.8.368 Aug 2026
Release notes
Open source →Fix missing commits in v2.8.35
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.35...v2.8.36
What's Changed in v2.8.36
This release focuses on streaming I/O: readers can now hand over a native stream instead of being read chunk by chunk, which removes a copy for blobs and a whole series of HTTP requests for remote archives read with range requests. It also fixes several zip64 and split-archive issues, and replaces the hand-maintained list of property names protected from minification with a derived one checked at build time.
New features
Reader#createReadable(options), introduced internally in 2.8.31, is now part of the documented API, with the newCreateReadableOptionstype (offset,size,chunkSize). Custom readers can override it to return a stream provided natively by the underlying data source instead of relying onreadUint8Array(). It also accepts a byte range, and no longer takes adiskNumberStartoption: split readers now use linear disk offsetsBlobReadernow returnsblob.stream(), orblob.slice(offset, end).stream()for a range, instead of reading the data inchunkSizeslices throughreadUint8Array()- When range requests are used, i.e. with
HttpRangeReaderor with theuseRangeHeaderandforceRangeRequestsoptions ofHttpReader, the data of an entry is now read with range requests of 16MB at most whose response bodies are streamed, instead of one range request perchunkSizebytes. Reading a 4GB entry sends 256 requests instead of 65536, and no request is ever sized after the entry. This requiresfetch, theuseXHRoption still reads the data chunk by chunk - Add the
maximumRangeSizeoption toHttpReaderandHttpRangeReaderto tune the size of these range requests, e.g. to lower it behind a proxy closing long-lived responses - These two changes only affect how the bytes are obtained: the chunks emitted by the platform are
still normalized to
chunkSizebefore reaching the codec, and the chunks received by writers are unchanged - Add the
closeDisk()method toSplitDataWriterto close the disk being written, the next disk being opened when more data is written - Add the
checkResourceChangesoption toHttpRangeOptionsand theERR_HTTP_RESOURCE_CHANGEDerror constant: range requests now detect a resource modified while being read by comparing theETag,Last-Modifiedand total size headers against the ones returned by the first request - Add
resetConfiguration()to restore the default configuration of zip.js
Behavior changes
checkResourceChangesdefaults totrue, so reading an HTTP resource that changes mid-read now throwsERR_HTTP_RESOURCE_CHANGEDinstead of silently producing corrupt data. Headers missing from the responses are ignored; note thatAccess-Control-Expose-Headersmust include them when the resource is fetched cross-origin- Entries requiring no codec work at all, i.e. no compression, no CRC-32 and no encryption, now
always bypass web workers. They previously went through a worker when
transferStreamswas enabled, i.e. by default. In practice this covers reading stored entries without thecheckSignatureoption, andpassThroughtransfers; entries needing the CRC-32, which includes every entry written byadd()withoutpassThrough, still go through a worker - Writers always receive a
Uint8Arraythat owns its entire buffer, sochunk.bufferinside a customwriteUint8Array()implementation is the chunk and nothing more - Non-split writers no longer get
diskNumber,diskOffset,availableSizeandmaxSizeproperties assigned on them byZipWriter - The
chunkSizeoption ofcreateReadable()defaults to thechunkSizevalue of the global configuration instead of a hardcoded 64KB. It only applies to the default implementation, the readers overriding it emit chunks sized by the platform
Fixes
- Fix
configure({ transferStreams })being silently ignored: the option was documented and honored per entry, but absent from the list of configurable properties, so it could not be set globally. It is now a global option defaulting totrue - Fix the
chunkSizeoption being ignored when the codec runs without a web worker: the codec output is now rechunked in that path too - Fix reading zip64 archives whose end of central directory record is not on the last disk: the disk number is now read from the zip64 end of central directory locator instead of being assumed
- Fix
prependZip()writing a wrong zip64 offset flag and leaking the disk numbers of the source archive into the central directory of the new archive - Fix
createSyncAccessHandlebeing mangled away in the minified builds, which brokecreateOPFSTempStream()indist/*.min.js,index.min.jsandindex-native.min.js - Make the error message reported for uncaught errors more informative
- Fix the web worker configuration of the test suite in
distmode - Fix the
build-devconfiguration, broken by the removal of mini-lz
Performance
- Make
ChunkStreamaccumulate a queue of pending chunks instead of concatenating into a growing buffer, which removes the copies that the iterative rechunking introduced in 2.8.27 still made on every incoming chunk - Copy a chunk sent through the web worker message protocol only when it is a partial view of its buffer, and stop copying chunks received from a worker
- Return the cached data view instead of a copy when an HTTP read is served from the buffer
- Remove the redundant copies made when reading extra fields
- Initialize the reader lazily when prepending a zip file, so nothing is read if no entry is added
- Let rollup drop the default configuration table from the web worker bundles
Build and packaging
- Derive the property names reserved from minification instead of maintaining them by hand: they
are now collected from the
lib.dom/lib.webworkerTypeScript declarations, the export table of the WASM module,index.d.ts, and an explicit list of the names crossingpostMessage - Fail the build when the set of mangled property names changes, so a name newly exposed at a boundary has to be audited instead of being silently renamed in one bundle only
- Add a test recording every property name crossing
postMessageduring worker round trips (deflate, signed, AES-256, AES-128, ZipCrypto, raw password, with and withouttransferStreams) and asserting each one is declared - Do not publish the
.githubfolder on NPM and JSR
Tests
- Add tests for zip64 split boundaries, HTTP range requests, HTTP resource changes, exact chunks
passed to writers, and
SplitDataWriteredge cases - Reset the configuration between tests
- Make the Deno
MessagePortleak repro runnable withdeno test(still reproduced with Deno 2.9.5)
Internal
- Centralize the array helpers in a
util/array.jsmodule
Documentation
- Add a bundle size section to the README documenting tree-shaking and the smaller entry points
- Clarify the meaning of the
nativesuffix in thedistREADME: every bundle uses the nativeCompressionStream/DecompressionStreamAPIs when available, the suffix names the implementation embedded for everything else - Clarify the comment describing the
Blob.slice()workaround inBlobReader
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.34...v2.8.35
- 2.8.358 Aug 2026
Release notes
Open source →What's Changed
This release focuses on streaming I/O: readers can now hand over a native stream instead of being read chunk by chunk, which removes a copy for blobs and a whole series of HTTP requests for remote archives read with range requests. It also fixes several zip64 and split-archive issues, and replaces the hand-maintained list of property names protected from minification with a derived one checked at build time.
New features
Reader#createReadable(options), introduced internally in 2.8.31, is now part of the documented API, with the newCreateReadableOptionstype (offset,size,chunkSize). Custom readers can override it to return a stream provided natively by the underlying data source instead of relying onreadUint8Array(). It also accepts a byte range, and no longer takes adiskNumberStartoption: split readers now use linear disk offsetsBlobReadernow returnsblob.stream(), orblob.slice(offset, end).stream()for a range, instead of reading the data inchunkSizeslices throughreadUint8Array()- When range requests are used, i.e. with
HttpRangeReaderor with theuseRangeHeaderandforceRangeRequestsoptions ofHttpReader, the data of an entry is now read with range requests of 16MB at most whose response bodies are streamed, instead of one range request perchunkSizebytes. Reading a 4GB entry sends 256 requests instead of 65536, and no request is ever sized after the entry. This requiresfetch, theuseXHRoption still reads the data chunk by chunk - Add the
maximumRangeSizeoption toHttpReaderandHttpRangeReaderto tune the size of these range requests, e.g. to lower it behind a proxy closing long-lived responses - These two changes only affect how the bytes are obtained: the chunks emitted by the platform are
still normalized to
chunkSizebefore reaching the codec, and the chunks received by writers are unchanged - Add the
closeDisk()method toSplitDataWriterto close the disk being written, the next disk being opened when more data is written - Add the
checkResourceChangesoption toHttpRangeOptionsand theERR_HTTP_RESOURCE_CHANGEDerror constant: range requests now detect a resource modified while being read by comparing theETag,Last-Modifiedand total size headers against the ones returned by the first request - Add
resetConfiguration()to restore the default configuration of zip.js
Behavior changes
checkResourceChangesdefaults totrue, so reading an HTTP resource that changes mid-read now throwsERR_HTTP_RESOURCE_CHANGEDinstead of silently producing corrupt data. Headers missing from the responses are ignored; note thatAccess-Control-Expose-Headersmust include them when the resource is fetched cross-origin- Entries requiring no codec work at all, i.e. no compression, no CRC-32 and no encryption, now
always bypass web workers. They previously went through a worker when
transferStreamswas enabled, i.e. by default. In practice this covers reading stored entries without thecheckSignatureoption, andpassThroughtransfers; entries needing the CRC-32, which includes every entry written byadd()withoutpassThrough, still go through a worker - Writers always receive a
Uint8Arraythat owns its entire buffer, sochunk.bufferinside a customwriteUint8Array()implementation is the chunk and nothing more - Non-split writers no longer get
diskNumber,diskOffset,availableSizeandmaxSizeproperties assigned on them byZipWriter - The
chunkSizeoption ofcreateReadable()defaults to thechunkSizevalue of the global configuration instead of a hardcoded 64KB. It only applies to the default implementation, the readers overriding it emit chunks sized by the platform
Fixes
- Fix
configure({ transferStreams })being silently ignored: the option was documented and honored per entry, but absent from the list of configurable properties, so it could not be set globally. It is now a global option defaulting totrue - Fix the
chunkSizeoption being ignored when the codec runs without a web worker: the codec output is now rechunked in that path too - Fix reading zip64 archives whose end of central directory record is not on the last disk: the disk number is now read from the zip64 end of central directory locator instead of being assumed
- Fix
prependZip()writing a wrong zip64 offset flag and leaking the disk numbers of the source archive into the central directory of the new archive - Fix
createSyncAccessHandlebeing mangled away in the minified builds, which brokecreateOPFSTempStream()indist/*.min.js,index.min.jsandindex-native.min.js - Make the error message reported for uncaught errors more informative
- Fix the web worker configuration of the test suite in
distmode - Fix the
build-devconfiguration, broken by the removal of mini-lz
Performance
- Make
ChunkStreamaccumulate a queue of pending chunks instead of concatenating into a growing buffer, which removes the copies that the iterative rechunking introduced in 2.8.27 still made on every incoming chunk - Copy a chunk sent through the web worker message protocol only when it is a partial view of its buffer, and stop copying chunks received from a worker
- Return the cached data view instead of a copy when an HTTP read is served from the buffer
- Remove the redundant copies made when reading extra fields
- Initialize the reader lazily when prepending a zip file, so nothing is read if no entry is added
- Let rollup drop the default configuration table from the web worker bundles
Build and packaging
- Derive the property names reserved from minification instead of maintaining them by hand: they
are now collected from the
lib.dom/lib.webworkerTypeScript declarations, the export table of the WASM module,index.d.ts, and an explicit list of the names crossingpostMessage - Fail the build when the set of mangled property names changes, so a name newly exposed at a boundary has to be audited instead of being silently renamed in one bundle only
- Add a test recording every property name crossing
postMessageduring worker round trips (deflate, signed, AES-256, AES-128, ZipCrypto, raw password, with and withouttransferStreams) and asserting each one is declared - Do not publish the
.githubfolder on NPM and JSR
Tests
- Add tests for zip64 split boundaries, HTTP range requests, HTTP resource changes, exact chunks
passed to writers, and
SplitDataWriteredge cases - Reset the configuration between tests
- Make the Deno
MessagePortleak repro runnable withdeno test(still reproduced with Deno 2.9.5)
Internal
- Centralize the array helpers in a
util/array.jsmodule
Documentation
- Add a bundle size section to the README documenting tree-shaking and the smaller entry points
- Clarify the meaning of the
nativesuffix in thedistREADME: every bundle uses the nativeCompressionStream/DecompressionStreamAPIs when available, the suffix names the implementation embedded for everything else - Clarify the comment describing the
Blob.slice()workaround inBlobReader
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.34...v2.8.35
- 2.8.3422 Jul 2026
Release notes
Open source →Bug fixes
- Fixed
zipglobal export name mangled away in minified UMD builds (e.g.dist/zip.min.js)
- Fixed
- 2.8.3321 Jul 2026
- 2.8.3220 Jul 2026
Release notes
Open source →New Features
- Added
createBlobTempStream, a ready-madecreateTempStreamfactory that spills the data of buffered entries into aBlobinstead of keeping it in memory. Memory is bounded in Chromium browsers; in other engines and non-browser runtimes theBlobis held in memory, socreateOPFSTempStreamor a file-backed implementation is preferable there. - Added
createSyncAccessHandleTempStream, an OPFS-backedcreateTempStreamfactory built onFileSystemSyncAccessHandle. It offers the same options and bounded-memory profile ascreateOPFSTempStreambut writes roughly 2.5× faster in Chromium and Firefox and reads back several times faster in Firefox and Safari, making disk-backed staging nearly as fast as the in-memory default (worker-only variant).
Bug Fixes
- Codec worker now copies chunks that do not own their underlying buffer before transferring them, preventing data corruption when a chunk's
ArrayBufferis shared or externally owned.
- Added
- 2.8.3115 Jul 2026
Release notes
Open source →New features
- Accept a non-constructable reader (a factory function, not only a
Readerclass) as theReaderof aZipFileEntry/ZipDirectoryEntry; the reader is now invoked with or withoutnewdepending on whether it exposes a prototype. - Export the
ERR_ITERATOR_COMPLETED_TOO_SOONandERR_WRITER_NOT_INITIALIZEDerror constants.
Bug fixes
- Add a dedicated
index.d.ctsso TypeScript resolves the correct types for the CommonJS entry point instead of falling back to the ESM declarations. - Fix invalid entries in the MIME type table: drop the bogus
x-font/pcf.Zand duplicatevideo/avifentries, correct the mis-keyedapplication/x-ms-installer(msi), and mapimage/avifto bothavifandavifs. - Align the JavaScript zlib module with the
windowBits: -16raw-stream convention.
Performance
- Build the MIME type map lazily from a compact JSON payload, so the table is only parsed when a MIME type is actually requested.
- Shrink the WebAssembly zlib module by ~8 KB (~16%) by merging the deflate64 path into the inflate code and generating the deflate trees at runtime.
- Reduce bundle size by mangling internal property names and shipping inline worker payloads deflate-compressed.
- Accept a non-constructable reader (a factory function, not only a
- 2.8.3014 Jul 2026
Release notes
Open source →New features
- Add the strictness option (
"strict" | "balanced" | "tolerant", default "balanced") andmaxAppendedDataSizeoption toZipReader, controlling how appended data, prepended stubs, and ambiguous end-of-central-directory records are handled.checkAmbiguity: true is now an alias for"strict". - Add
createOPFSTempStream()and thecreateTempStreamwriter option to spill large buffered entries to the OPFS instead of memory, with an optionaldispose()hook released on every exit path. - Add
FS#exportFileSystemHandle()andZipDirectoryEntry#exportFileSystemHandle()to write an entry tree out to aFileSystemDirectoryHandle.
Security: End-of-central-directory selection
- Select the last end-anchored record that points to a central directory instead of the first signature scanned from the end; refuse genuine comment-cloak polyglots with
ERR_AMBIGUOUS_ARCHIVE. - Rank record reachability so empty or saturated bytes at the end of a comment can no longer forge a second record; genuine empty and zip64 archives still open.
- Prefer the reconciled central-directory offset over a stale one, so an identical-layout append remnant exposes the appended directory rather than the previous one.
- Apply the reachability check to the fallback scan, so a stray signature in appended data no longer hijacks recovery under tolerant. Security
- Bound the reachability probes to prevent a comment stuffed with unreachable records from amplifying into thousands of (potentially remote) reads on random-access readers such as
HttpRangeReader.
Performance
- Speed up CRC-32 with a slice-by-8 implementation and by harvesting the CRC from the gzip trailer on the native
CompressionStream.
- Add the strictness option (
- 2.8.2913 Jul 2026
Release notes
Open source →Bug fixes
-
Bounded memory when streaming large entries. The compression path now applies backpressure through the codec, pacing the input to the rate the compressor consumes it. Previously, writing a large entry could grow memory in proportion to the entry size — a 256 MB file peaked around 390 MB — because the platform's native
CompressionStreamdoes not apply writable backpressure in Node.js and Bun. Peak memory is now flat regardless of entry size (~99 MB for that same 256 MB file), with no throughput cost. (d18f4e3a) -
A provided zlib codec is no longer discarded when the WASM module is unavailable. If you supply a self-contained
CompressionStreamZlib/DecompressionStreamZlib(e.g. the pure-JS port) withuseCompressionStream: falseand no WASM module (wasmURI: null, or a failed load), the library used to silently fall back to the nativeCompressionStream. Your codec is now honored; the native fallback applies only when the codec that would actually run depends on the WASM module. (a67c54e1)
Other
- Added a reproducible benchmark suite (
benchmarks/) andBENCHMARKS.mdcomparing@zip.js/zip.jsagainst jszip, fflate and archiver across compression, decompression and disk-to-disk streaming. (65178e72) - Tests: the crypto error-path tests opt out of Deno's resource sanitizer, working around a Deno bug where cancelling a transferred, still-open
ReadableStreamleaks aMessagePort(reported upstream — denoland/deno#36015). (454f79ec)
-
- 2.8.2813 Jul 2026
Release notes
Open source →What's Changed
This release contains an unusually large number of fixes resulting from an in-depth audit of the codebase, along with several new features. All fixes are covered by new regression tests, and the produced archives were validated against external tools (7z, unzip, ditto, Python).
New features
- Add the
checkAmbiguityoption toZipReaderto detect and reject ambiguous archives: concatenated archives, trailing central directory records, mismatched zip64 records, duplicate filenames, and local file headers contradicting the central directory - Add the
fetchoption toHttpReaderandHttpRangeReaderin https://github.com/gildas-lormeau/zip.js/issues/655 - Add the
workerStarvationTimeoutoption toconfigure()and run starved codecs without worker to prevent deadlocks - Support the
offsetoption with split archives - Support the
rawPasswordoption with ZipCrypto - Add
index.cjs/index-native.cjssubpath exports and areact-nativecondition inpackage.jsonin https://github.com/gildas-lormeau/zip.js/issues/651 - Add the
ERR_AMBIGUOUS_ARCHIVE,ERR_INVALID_COMPRESSED_DATAandERR_UNDEFINED_READERerror constants
Deflate64
- Fix decompression of entries containing matches longer than 258 bytes (length code 285) in https://github.com/gildas-lormeau/zip.js/issues/661 — the wasm fix was contributed by @Chagrins in https://github.com/gildas-lormeau/zlib-streams/pull/1
- Add the
deflate64option to the reserved properties of inline web workers (web workers silently decompressed Deflate64 entries with the deflate32 codec)
Security
- Always verify the AES authentication code when decrypting entries
- Compare the ZipCrypto password verification byte in constant time
- Throw
ERR_UNSUPPORTED_CRYPTO_APIinstead of falling back to an insecure pseudo-random generator when the Web Crypto API is unavailable, and remove the unused pseudo-random fallback from sjcl.js - Detect overlapping entries regardless of check order, and when reading entries multiple times
- Throw zip.js errors instead of
RangeErroron malformed archives
Behavior changes
No API was removed or changed, but some intentional behavior changes are worth noting:
dataDescriptorSignaturenow defaults totrue: archives grow by 4 bytes per entry when data descriptors are used, and output bytes differ (fixes extraction with macOSditto)add()withpassThroughset and no reader throwsERR_UNDEFINED_READERinstead of silently producing corrupt outputadd()with an unsupportedcompressionMethodthrows instead of producing an unreadable entry- Extra fields totaling over 64KB throw instead of writing a corrupt header
add(name)without reader now writes a well-defined empty stored entry- Extended timestamps outside the signed 32-bit range are omitted (the NTFS field carries the exact value); reads treat the field as signed per the spec
FS.getById()no longer returns detached entries, and options objects passed toadd(),addFile(),exportZip()andHttpRangeReaderare no longer mutated- The
FStype declaration no longer pretends to extendZipDirectoryEntry(the runtime never did); TypeScript code relying on the incorrect declaration may need adjusting
ZipReader fixes
- Fix reading split zip files with a central directory spanning multiple segments (zip32 and zip64)
- Fix reading prepended data with zip64 offsets
- Fix extracting prepended data with multiple entries
- Fix CRC-32 and AES compression method in prepended entries
- Read the entry count from the total field in the end of central directory record
- Fix
getData()resettinglastAccessDateandcreationDateon entries - Derive the directory flag from the final filename after unicode path override
- Propagate entry data errors in
ZipReaderStream - Fix AES decryption buffering
- Fix cp437/BOM text decoding and harden crypto helpers
- Fix central directory, encrypted flag and timestamp handling
- Support readers returning subarray views
- Avoid mutating caller options in
checkPassword
ZipWriter fixes
- Fix zip64 detection for encrypted entries
- Fix the "version needed" field for zip64 offset entries
- Throw an error when sizes overflow without zip64
- Use zip64 when values equal the 32-bit/16-bit sentinels
- Write the local zip64 extra field for streamed zip64 entries
- Fix the local zip64 compressed size for encrypted STORE and passthrough entries
- Account for the zip64 trailer in the central directory length
- Fix crash and offsets when headers cross split segment boundaries
- Fix double-counted size in the split data writer and exact-fill handling in
SplitDataWriter - Fix duplicate filename check after a failed
add() - Fix offset tracking after failed entry writes
- Advance the offset by the bytes actually flushed on aborted buffered writes
- Preserve
add()call order for concurrently added entries - Fix deadlock between concurrent
add()calls andcreateTempStream - Propagate close errors in
ZipWriterStreamtransform - Fix directory name duplicates, comment validation and
ZipWriterStreamerrors - Fix
prependZipcompression level bits andcompressionMethod/levelconsistency - Fix USDZ extra field length off by 2
- Write the Info-ZIP Unix (0x7855) extra field per spec (fixed 2-byte uid/gid)
- Write extra field type and length as 16-bit little-endian
- Harden entry metadata written by
ZipWriter
Web workers fixes
- Release the writable lock when a worker task fails
- Fix worker slot double-release and stale messages on task failure
- Fix variable shadowing breaking
error.outputSizein web workers - Handle worker loading errors and honor
workerURIconfiguration changes - Reset the terminated flag when creating a new worker
- Fix codec stream issues with small chunk sizes, opaque errors and wasm memory leaks
Filesystem (zip.fs) fixes
- Fix
exportZip()silently dropping entries when a reader fails - Fix
importZip()directory metadata, name collisions and read options - Reset the filesystem tree in
FS.importZip()like other imports - Fix
remove()leaving descendant entries registered in the filesystem - Register entries only when they are attached to the tree
- Avoid stack overflows when exporting deeply nested trees
find(): fall back to exact full-name match for verbatim slash namesgetArrayBuffer(): read viagetUint8Arrayso all entry data types work- Do not mutate the options object passed to
exportZip() - Add
FS.exportZip()delegation and default its options - Fix the
FStype declaration to match its actual runtime surface
I/O fixes
- Fix byte order in
Data64URIWriterwith sub-3-byte writes - Fix HTTP, split disk and blob I/O issues, including
HttpReaderend-of-central-directory caching
Performance
- Make
ChunkStreamiterative and tolerate invalid chunk sizes (supersedes https://github.com/gildas-lormeau/zip.js/pull/660 — reading a 50MB file with a small chunk size went from ~4900ms to ~80ms) - Avoid copying the entry map on each
add()
Documentation
- Document that entry filenames must use forward slashes as separator in https://github.com/gildas-lormeau/zip.js/issues/654
- Fix dash/dot switchup in README by @KTibow in https://github.com/gildas-lormeau/zip.js/pull/650
Credits
- Most of the bugs fixed in this release were found and fixed by Claude (Fable) during several in-depth reviews of the codebase, which also produced the new regression tests
- @Chagrins fixed the length-code extra bits bug in the Inflate wasm implementation (https://github.com/gildas-lormeau/zlib-streams/pull/1), part of the fix for https://github.com/gildas-lormeau/zip.js/issues/661
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.26...v2.8.28
- Add the
- 2.8.261 Apr 2026
Release notes
Open source →What's Changed
- Fix return types for entry progress callbacks by @EvanHahn in https://github.com/gildas-lormeau/zip.js/pull/648
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.25...v2.8.26
- 2.8.251 Apr 2026
Release notes
Open source →- fixed
HttpRangeReaderbug when reading compressed sizes multiples of 65536 (see #646) - updated dev dependencies
- fixed
- 2.8.245 Mar 2026
- 2.8.235 Mar 2026
Release notes
Open source →- Fixed zip64 auto-detection with
keepOrderset tofalseinZipWriter
Zip files containing entries larger than 4 GB or larger than 4 GB are now handled correctly when entries are written in parallel with
keepOrderset tofalse. The zip64 extra field in the central directory is now built from actual offset and disk number values at close time, rather than being predicted at entry creation time.- Updated dev dependencies
- Fixed zip64 auto-detection with
- 2.8.222 Mar 2026
Release notes
Open source →Fixed support of option
keepOrderwhen set tofalseinZipWriter(the option is set totrueby default) - 2.8.2118 Feb 2026
Release notes
Open source →Implemented workaround (feature test) for
DOMExceptionserialization bug in Deno version 2.6.x (fix https://github.com/gildas-lormeau/zip.js/issues/636) - 2.8.2011 Feb 2026
Release notes
Open source →- Fixed inadvertently removed createTempStream option into
ZipWriter - Added transferStreams support in
ZipWriter
- Fixed inadvertently removed createTempStream option into
- 2.8.1910 Feb 2026
Release notes
Open source →Removed unwanted
web-workerdependency inadvertently added in thepackage.jsonfile of version2.8.17 - 2.8.1810 Feb 2026
Release notes
Open source →- Added
createTempStreamoption toZipWriter, used when adding entries in parallel, for custom temporary buffered write storage (e.g. filesystem, OPFS, network) instead of the default in-memoryTransformStream(see this test for a usage example) - Improved buffering implementation in
ZipWriterby removing theBlob/Responseusage - Fixed minor regression introduced in version
2.8.16when the web worker URI is not a data or blob URI (workers were not working with code using ES6import) (see related test)
- Added
- 2.8.175 Feb 2026
- 2.8.1628 Jan 2026
Release notes
Open source →- Implemented lazy evaluation of the existence of the
WorkerAPI to facilitate the use of polyfills on Node.js (e.g.web-worker), see https://github.com/gildas-lormeau/zip.js/discussions/635#discussioncomment-15633490 for more info - Updated dev dependencies
- Implemented lazy evaluation of the existence of the
- 2.8.1513 Jan 2026
Release notes
Open source →- Fixed NPM Access Token expiration issue (see https://github.com/gildas-lormeau/zip.js/issues/633)
- Fixed minor issues in the documentation
- 2.8.147 Jan 2026
Release notes
Open source →- Fixed compressed size value in local headers when creating zip files using ZIP64 implicitly, i.e. when the
zip64option is not set totrue(see https://github.com/gildas-lormeau/zip.js/issues/627) and, for example, creating ZIP files weighting more than 4GB. - Fixed potential empty ZIP64 extra field in local headers
- Fixed compressed size value in local headers when creating zip files using ZIP64 implicitly, i.e. when the
- 2.8.135 Jan 2026
Release notes
Open source →- Fixed regression (briefly) introduced in version 2.8.12 leading to missing data in ZIP64 Extra field of the central directory entries.
- 2.8.1120 Nov 2025
Release notes
Open source →What's Changed
- Add missing typings for
useWebWorkersanduseCompressionStreamtoZipWriterConstructorOptionsby @ws333 in https://github.com/gildas-lormeau/zip.js/pull/616
New Contributors
- @ws333 made their first contribution in https://github.com/gildas-lormeau/zip.js/pull/616
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.10...v2.8.11
- Add missing typings for
- 2.8.107 Nov 2025
- 2.8.97 Nov 2025
Release notes
Open source →Fixed potential "Error: process error:-50331648" when decompressing deflate64 data
- 2.8.816 Oct 2025
Release notes
Open source →- fixed entry comments encoding in
ZipWriter(see #609) - improved
Uint8ArrayWriterbuffer growth strategy (see #610) - improved Unix/MS-DOS metadata support in
ZipWriterandZipReader:- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#externalfileattributes
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#msdosattributes
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#gid
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#uid
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#sticky
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#unixmode
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#setgid
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#setuid
- https://gildas-lormeau.github.io/zip.js/api/interfaces/EntryMetaData.html#unixexternalupper
- https://gildas-lormeau.github.io/zip.js/api/interfaces/ZipWriterConstructorOptions.html#gid
- https://gildas-lormeau.github.io/zip.js/api/interfaces/ZipWriterConstructorOptions.html#uid
- https://gildas-lormeau.github.io/zip.js/api/interfaces/ZipWriterConstructorOptions.html#sticky
- https://gildas-lormeau.github.io/zip.js/api/interfaces/ZipWriterConstructorOptions.html#unixextrafieldtype
- https://gildas-lormeau.github.io/zip.js/api/interfaces/ZipWriterConstructorOptions.html#unixmode
- https://gildas-lormeau.github.io/zip.js/api/interfaces/ZipWriterConstructorOptions.html#setgid
- https://gildas-lormeau.github.io/zip.js/api/interfaces/ZipWriterConstructorOptions.html#setuid
- fixed entry comments encoding in
- 2.8.74 Oct 2025
Release notes
Open source →Improved bugfix of
v2.8.6when enqueuing sub-array ofUint8Arrayinstances into aWritableStream(with web workers enabled). - 2.8.63 Oct 2025
Release notes
Open source →Fixed bug when enqueuing sub-array of
Uint8Arrayinstances into aWritableStreamin environments supporting web workers and transferable objects (see https://github.com/gildas-lormeau/zip.js/discussions/606) - 2.8.53 Oct 2025
Release notes
Open source →- Optimized size of built files (approx 7%) when using native (i.e. pure JS) compression/decompression (see files/exports suffixed with
-native). The code of web workers is now compressed at built time with the mini-compressor used to compress the code of the WASM module (see mini-lz.js), and decompressed at runtime. - Fixed path to the WASM module in the default configuration
- Optimized size of built files (approx 7%) when using native (i.e. pure JS) compression/decompression (see files/exports suffixed with
- 2.8.41 Oct 2025
Release notes
Open source →What's Changed
- Added export of web workers and WASM module files in the
package.jsonfile by @jschwartzbeck in https://github.com/gildas-lormeau/zip.js/pull/607 - Fixed potential race condition when initializing the WASM module in the main thread (i.e. with web workers disabled)
New Contributors
- @jschwartzbeck made their first contribution in https://github.com/gildas-lormeau/zip.js/pull/607
- Added export of web workers and WASM module files in the
- 2.8.329 Sept 2025
Release notes
Open source →- Added support of JavaScript-based implementation (based on zlib) of Compression Streams API (see https://github.com/gildas-lormeau/zlib-streams-ts) as an alternative to the default WASM-based implementation. See the new
exportsproperty in thepackage.jsonfile and files in the/distfolder, JavaScript-based implementations are suffixed with-native(e.g../index-native.js). - Fixed
mallocerror in environments not fully supporting WASM (see https://github.com/gildas-lormeau/zip.js/issues/605)
- Added support of JavaScript-based implementation (based on zlib) of Compression Streams API (see https://github.com/gildas-lormeau/zlib-streams-ts) as an alternative to the default WASM-based implementation. See the new
- 2.8.29 Sept 2025
Release notes
Open source →New in version 2.8
- WebAssembly Integration
- Replaced JavaScript compression and decompression implementation with WebAssembly zlib module (https://github.com/gildas-lormeau/zlib-streams. This improves security and performance when native
CompressionStreamandDecompressionStreamAPIs are not used (for example, when using custom compression levels or decompressing deflate64 data) or unavailable. - Added the
wasmURIconfiguration property to provide the module URI if necessary (for example, when the CSP blocks data URIs).
- Deflate64 Decompression Support
- Added support of proprietary Deflate64 decompression algorithm which can be used by Windows when compressing large files (see https://github.com/gildas-lormeau/zip.js/issues/517)
- Simplified Configuration
- Replaced the complex
workerScriptsconfiguration with a singleworkerURIproperty, making it easier to set up web workers. - Default values for
workerURIandwasmURIare provided, reducing the need for manual configuration when building code.
- Improved Type Safety and Clarity
- Refined TypeScript definitions, including clearer distinctions between
DirectoryEntryandFileEntry. - Deprecated and removed legacy APIs and classes, streamlining the library for current use cases.
Breaking Changes
- Worker Configuration Changes:
- The
workerScriptsproperty in the configuration was removed and replaced withworkerURI(andwasmURIfor the WASM module).
- Stream Implementation Updates:
- The
DeflateandInflateproperties in the configuration were removed and replaced withCompressionStreamandDecompressionStream.
- Deprecated Classes Removed:
- The deprecated classes
SplitZipReaderandSplitZipWriterwere removed.
- Event-Based Codec Support Removed:
- Interfaces and classes related to event-based codecs, such as
EventBasedZipLibrary,initShimAsyncCodec, andEventBasedCodecwere removed.
- Entry Metadata Changes in
index.d.ts:
- The
directoryproperty was removed fromEntryMetaDataand moved toDirectoryEntryandFileEntryinterfaces.
What's Changed in v2.8.2
- Fix edge cases of uncompressed size check by @0f-0b in https://github.com/gildas-lormeau/zip.js/pull/600
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.8.1...v2.8.2
- 2.8.18 Sept 2025
Release notes
Open source →New in version 2.8
-
WebAssembly Integration
- Added support for WebAssembly module based on zlib (see https://github.com/gildas-lormeau/zlib-streams) to enhance performance for compression and decompression tasks when
CompressionStreamandDecompressionStreamare not used.
- Added support for WebAssembly module based on zlib (see https://github.com/gildas-lormeau/zlib-streams) to enhance performance for compression and decompression tasks when
-
Deflate64 decompression support
- Added support of proprietary Deflate64 decompression algorithm which used by Windows when compressing large files (see https://github.com/gildas-lormeau/zip.js/issues/517)
-
Simplified Configuration
- Replaced the complex
workerScriptsconfiguration with a singleworkerURIproperty, making it easier to set up web workers. - Default values for
workerURIandwasmURIare provided, reducing the need for manual configuration.
- Replaced the complex
-
Improved Type Safety and Clarity
- Refined TypeScript definitions, including clearer distinctions between
DirectoryEntryandFileEntry. - Deprecated and removed legacy APIs and classes, streamlining the library for current use cases.
- Refined TypeScript definitions, including clearer distinctions between
Breaking Changes
-
Worker Configuration Changes:
- The
workerScriptsproperty in the configuration was removed and replaced withworkerURIandwasmURI.
- The
-
Stream Implementation Updates:
- The
DeflateandInflateproperties in the configuration were removed and replaced withCompressionStreamandDecompressionStream.
- The
-
Deprecated Classes Removed:
- The deprecated classes
SplitZipReaderandSplitZipWriterwere removed.
- The deprecated classes
-
Event-Based Codec Support Removed:
- Interfaces and classes related to event-based codecs, such as
EventBasedZipLibrary,initShimAsyncCodec, andEventBasedCodecwere removed.
- Interfaces and classes related to event-based codecs, such as
-
Entry Metadata Changes:
- The
directoryproperty was removed fromEntryMetaDataand moved toDirectoryEntryandFileEntryinterfaces.
- The
v2.8.1: Fixed exports in
package.json(see https://github.com/gildas-lormeau/zip.js/issues/599) -
- 2.8.08 Sept 2025
Release notes
Open source →New in version 2.8
-
WebAssembly Integration
- Added support for WebAssembly module based on zlib (see https://github.com/gildas-lormeau/zlib-streams) to enhance performance for compression and decompression tasks when
CompressionStreamandDecompressionStreamare not used.
- Added support for WebAssembly module based on zlib (see https://github.com/gildas-lormeau/zlib-streams) to enhance performance for compression and decompression tasks when
-
Deflate64 decompression support
- Added support of proprietary Deflate64 decompression algorithm which used by Windows when compressing large files (see https://github.com/gildas-lormeau/zip.js/issues/517)
-
Simplified Configuration
- Replaced the complex
workerScriptsconfiguration with a singleworkerURIproperty, making it easier to set up web workers. - Default values for
workerURIandwasmURIare provided, reducing the need for manual configuration.
- Replaced the complex
-
Improved Type Safety and Clarity
- Refined TypeScript definitions, including clearer distinctions between
DirectoryEntryandFileEntry. - Deprecated and removed legacy APIs and classes, streamlining the library for current use cases.
- Refined TypeScript definitions, including clearer distinctions between
Breaking Changes
-
Worker Configuration Changes:
- The
workerScriptsproperty in the configuration was removed and replaced withworkerURIandwasmURI.
- The
-
Stream Implementation Updates:
- The
DeflateandInflateproperties in the configuration were removed and replaced withCompressionStreamandDecompressionStream.
- The
-
Deprecated Classes Removed:
- The deprecated classes
SplitZipReaderandSplitZipWriterwere removed.
- The deprecated classes
-
Event-Based Codec Support Removed:
- Interfaces and classes related to event-based codecs, such as
EventBasedZipLibrary,initShimAsyncCodec, andEventBasedCodecwere removed.
- Interfaces and classes related to event-based codecs, such as
-
Entry Metadata Changes:
- The
directoryproperty was removed fromEntryMetaDataand moved toDirectoryEntryandFileEntryinterfaces.
- The
-
- 2.7.731 Sept 2025
Release notes
Open source →What's Changed
- fix: type of Uint8ArrayWriter by @targos in https://github.com/gildas-lormeau/zip.js/pull/594
New Contributors
- @targos made their first contribution in https://github.com/gildas-lormeau/zip.js/pull/594
Full Changelog: https://github.com/gildas-lormeau/zip.js/compare/v2.7.72...v2.7.73
- 2.7.724 Aug 2025
Release notes
Open source →- Fixed regression when using zip.js in Node < 21.2 with the option
useCompressionStreamnot explicitly set tofalse(Node.js does not support thedeflate-rawformat of theCompressionStreamAPI in version < 21.2) - Removed useless files from the JSR repository
- Fixed regression when using zip.js in Node < 21.2 with the option