https://docs.rs/lzma-rs binding to Node.js via https://napi.rs
TypeScript
21
504 commits
updated Oct 12, 2026
@napi-rs/lzmalzma / lzma2 / xz compression for Node.js and the browser, backed by the pure-Rust lzma-rust2 crate via napi-rs.
π Help me to become a full-time open-source developer by sponsoring me on Github
yarn add @napi-rs/lzma
engines.node is ^22.20 || ^24.12 || >=25 β a deliberately non-contiguous range:
| Range | Supported | Note |
|---|---|---|
< 22.20 | no | |
22.20 β 22.x | yes | tested in CI |
23.x | no | reached end-of-life 2025-06-01 |
24.0 β 24.11 | no | |
24.12 β 24.x | yes | tested in CI |
25.x | yes | permitted, but reached end-of-life 2026-06-01 |
26 and later | yes | not in the CI matrix |
Release-line status as of 2026-07-10: 22 is Maintenance LTS, 24 is Active LTS, 26 is Current.
Why these exact cutoffs? They are a support policy, not a technical limit. The only hard
floor in the shipped code is Node 22.12, where require(esm) became unflagged β main.js
loads stream-polyfill.mjs with require. The native binding itself asks for nothing newer
than Node-API 5. The ^22.20 || ^24.12 || >=25 range was inherited from the test toolchain
(ava declares ^22.20 || ^24.12 || >=26) and predates the require(esm) code by two months.
Node 23 and 24.0β24.11 are dropped by policy; the code runs on them.
| Rust triple | Platform | CI |
|---|---|---|
x86_64-pc-windows-msvc | Windows x64 | tested β node 22, 24 |
aarch64-pc-windows-msvc | Windows arm64 | tested β node 22, 24 |
i686-pc-windows-msvc | Windows x32 | tested β node 22 (x86), --serial |
x86_64-apple-darwin | macOS x64 | tested β node 22, 24 |
aarch64-apple-darwin | macOS arm64 | tested β node 22, 24 |
x86_64-unknown-linux-gnu | Linux x64 gnu | tested β node 22, 24 |
x86_64-unknown-linux-musl | Linux x64 musl | tested β node 22, 24 |
aarch64-unknown-linux-gnu | Linux arm64 gnu | tested β node 22, 24 |
aarch64-unknown-linux-musl | Linux arm64 musl | tested β node 22, 24 |
armv7-unknown-linux-gnueabihf | Linux armv7 gnu | tested β node 22 only, --serial |
x86_64-unknown-freebsd | FreeBSD x64 | tested β node version unpinned |
powerpc64le-unknown-linux-gnu | Linux ppc64le | non-blocking (continue-on-error) |
s390x-unknown-linux-gnu | Linux s390x | non-blocking (continue-on-error) |
riscv64gc-unknown-linux-gnu | Linux riscv64 | built, not tested |
aarch64-linux-android | Android arm64 | built, not tested |
arm-linux-androideabi | Android armv7 | built, not tested |
wasm32-wasi-preview1-threads | wasm32-wasi, browser | built, not tested |
Seventeen targets: eleven CI-tested, two non-blocking, four built but not exercised.
Bundlers resolve @napi-rs/lzma-wasm32-wasi through the browser export condition. The wasm
build allocates shared memory and spawns worker threads, so SharedArrayBuffer must be
available β the page has to be
cross-origin isolated,
served with Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp.
import { compress, decompress } from '@napi-rs/lzma/xz'
const compressed = await compress('Hello napi-rs π')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs π
import { compress, decompress } from '@napi-rs/lzma/lzma'
const compressed = await compress('Hello napi-rs π')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs π
import { compress, decompress } from '@napi-rs/lzma/lzma2'
const compressed = await compress('Hello napi-rs π')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs π
Every namespace (xz, lzma, lzma2) additionally exposes an incremental streaming API. The one-shot compress / decompress above are unchanged; streaming is purely additive.
Feed data chunk-by-chunk with update() and flush with finish(). The valid stream is the concatenation of every update() output plus the finish() tail.
import { Compressor, Decompressor } from '@napi-rs/lzma/xz'
const compressor = new Compressor({ preset: 6 })
const parts = [compressor.update('Hello '), compressor.update('napi-rs π'), await compressor.finish()]
const compressed = Buffer.concat(parts)
const decompressor = new Decompressor()
const restored = Buffer.concat([decompressor.update(compressed), await decompressor.finish()])
console.log(restored.toString('utf8')) // Hello napi-rs π
The top-level entry re-exports the same classes with format-qualified names: XzCompressor / XzDecompressor, LzmaCompressor / LzmaDecompressor, Lzma2Compressor / Lzma2Decompressor.
Each namespace exposes a WHATWG Web Streams API β Uint8Array in, compressed Uint8Array out:
import { compressStream, decompressStream } from '@napi-rs/lzma/xz'
const compressed = source.pipeThrough(new TransformStream()) // any ReadableStream<Uint8Array>
const restored = decompressStream(compressStream(source))
input must be a WHATWG ReadableStream; wrap a Node Readable with Readable.toWeb().
For ready-to-pipe Node streams, each namespace subpath exports createCompressStream() / createDecompressStream(), which return a Node Duplex:
import { createReadStream, createWriteStream } from 'node:fs'
import { createCompressStream } from '@napi-rs/lzma/xz'
createReadStream('input.txt').pipe(createCompressStream()).pipe(createWriteStream('input.txt.xz'))
lzma-rust2 crate (previously lzma-rs). The one-shot API behavior is unchanged and the output remains standard, liblzma-compatible .xz / .lzma.compressStream / decompressStream transforms are compiled out of the wasm build, so under wasm the Web Streams API transparently falls back to a buffered polyfill (it drains the input, runs the class API, and emits a single chunk). The Node Duplex factories are Node-only and are not part of the browser entry.{ dictSize } on both the compressor and the decompressor β they must agree. A decoder configured with a smaller dictionary than the encoder fails cleanly with an InvalidArg error; it never silently corrupts the output.lzma / lzma2 stream decoders complete at the in-band end marker and ignore any trailing bytes after a complete frame; the xz decoder validates its framed trailer.Cancelling a Web compressStream / decompressStream output while a read() is still pending on a stalled or never-ending input can leak one worker thread until the process exits. Normal cancels β before reading, after data has flowed, or on any finite input β are unaffected. (Root cause: napi's stream cancel cannot interrupt a pull that is already in flight; a full fix needs an upstream napi cancel hook.)
https://docs.rs/lzma-rs binding to Node.js via https://napi.rs
TypeScript
21
504 commits
updated Oct 12, 2026
@napi-rs/lzmalzma / lzma2 / xz compression for Node.js and the browser, backed by the pure-Rust lzma-rust2 crate via napi-rs.
π Help me to become a full-time open-source developer by sponsoring me on Github
yarn add @napi-rs/lzma
engines.node is ^22.20 || ^24.12 || >=25 β a deliberately non-contiguous range:
| Range | Supported | Note |
|---|---|---|
< 22.20 | no | |
22.20 β 22.x | yes | tested in CI |
23.x | no | reached end-of-life 2025-06-01 |
24.0 β 24.11 | no | |
24.12 β 24.x | yes | tested in CI |
25.x | yes | permitted, but reached end-of-life 2026-06-01 |
26 and later | yes | not in the CI matrix |
Release-line status as of 2026-07-10: 22 is Maintenance LTS, 24 is Active LTS, 26 is Current.
Why these exact cutoffs? They are a support policy, not a technical limit. The only hard
floor in the shipped code is Node 22.12, where require(esm) became unflagged β main.js
loads stream-polyfill.mjs with require. The native binding itself asks for nothing newer
than Node-API 5. The ^22.20 || ^24.12 || >=25 range was inherited from the test toolchain
(ava declares ^22.20 || ^24.12 || >=26) and predates the require(esm) code by two months.
Node 23 and 24.0β24.11 are dropped by policy; the code runs on them.
| Rust triple | Platform | CI |
|---|---|---|
x86_64-pc-windows-msvc | Windows x64 | tested β node 22, 24 |
aarch64-pc-windows-msvc | Windows arm64 | tested β node 22, 24 |
i686-pc-windows-msvc | Windows x32 | tested β node 22 (x86), --serial |
x86_64-apple-darwin | macOS x64 | tested β node 22, 24 |
aarch64-apple-darwin | macOS arm64 | tested β node 22, 24 |
x86_64-unknown-linux-gnu | Linux x64 gnu | tested β node 22, 24 |
x86_64-unknown-linux-musl | Linux x64 musl | tested β node 22, 24 |
aarch64-unknown-linux-gnu | Linux arm64 gnu | tested β node 22, 24 |
aarch64-unknown-linux-musl | Linux arm64 musl | tested β node 22, 24 |
armv7-unknown-linux-gnueabihf | Linux armv7 gnu | tested β node 22 only, --serial |
x86_64-unknown-freebsd | FreeBSD x64 | tested β node version unpinned |
powerpc64le-unknown-linux-gnu | Linux ppc64le | non-blocking (continue-on-error) |
s390x-unknown-linux-gnu | Linux s390x | non-blocking (continue-on-error) |
riscv64gc-unknown-linux-gnu | Linux riscv64 | built, not tested |
aarch64-linux-android | Android arm64 | built, not tested |
arm-linux-androideabi | Android armv7 | built, not tested |
wasm32-wasi-preview1-threads | wasm32-wasi, browser | built, not tested |
Seventeen targets: eleven CI-tested, two non-blocking, four built but not exercised.
Bundlers resolve @napi-rs/lzma-wasm32-wasi through the browser export condition. The wasm
build allocates shared memory and spawns worker threads, so SharedArrayBuffer must be
available β the page has to be
cross-origin isolated,
served with Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp.
import { compress, decompress } from '@napi-rs/lzma/xz'
const compressed = await compress('Hello napi-rs π')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs π
import { compress, decompress } from '@napi-rs/lzma/lzma'
const compressed = await compress('Hello napi-rs π')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs π
import { compress, decompress } from '@napi-rs/lzma/lzma2'
const compressed = await compress('Hello napi-rs π')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs π
Every namespace (xz, lzma, lzma2) additionally exposes an incremental streaming API. The one-shot compress / decompress above are unchanged; streaming is purely additive.
Feed data chunk-by-chunk with update() and flush with finish(). The valid stream is the concatenation of every update() output plus the finish() tail.
import { Compressor, Decompressor } from '@napi-rs/lzma/xz'
const compressor = new Compressor({ preset: 6 })
const parts = [compressor.update('Hello '), compressor.update('napi-rs π'), await compressor.finish()]
const compressed = Buffer.concat(parts)
const decompressor = new Decompressor()
const restored = Buffer.concat([decompressor.update(compressed), await decompressor.finish()])
console.log(restored.toString('utf8')) // Hello napi-rs π
The top-level entry re-exports the same classes with format-qualified names: XzCompressor / XzDecompressor, LzmaCompressor / LzmaDecompressor, Lzma2Compressor / Lzma2Decompressor.
Each namespace exposes a WHATWG Web Streams API β Uint8Array in, compressed Uint8Array out:
import { compressStream, decompressStream } from '@napi-rs/lzma/xz'
const compressed = source.pipeThrough(new TransformStream()) // any ReadableStream<Uint8Array>
const restored = decompressStream(compressStream(source))
input must be a WHATWG ReadableStream; wrap a Node Readable with Readable.toWeb().
For ready-to-pipe Node streams, each namespace subpath exports createCompressStream() / createDecompressStream(), which return a Node Duplex:
import { createReadStream, createWriteStream } from 'node:fs'
import { createCompressStream } from '@napi-rs/lzma/xz'
createReadStream('input.txt').pipe(createCompressStream()).pipe(createWriteStream('input.txt.xz'))
lzma-rust2 crate (previously lzma-rs). The one-shot API behavior is unchanged and the output remains standard, liblzma-compatible .xz / .lzma.compressStream / decompressStream transforms are compiled out of the wasm build, so under wasm the Web Streams API transparently falls back to a buffered polyfill (it drains the input, runs the class API, and emits a single chunk). The Node Duplex factories are Node-only and are not part of the browser entry.{ dictSize } on both the compressor and the decompressor β they must agree. A decoder configured with a smaller dictionary than the encoder fails cleanly with an InvalidArg error; it never silently corrupts the output.lzma / lzma2 stream decoders complete at the in-band end marker and ignore any trailing bytes after a complete frame; the xz decoder validates its framed trailer.Cancelling a Web compressStream / decompressStream output while a read() is still pending on a stalled or never-ending input can leak one worker thread until the process exits. Normal cancels β before reading, after data has flowed, or on any finite input β are unaffected. (Root cause: napi's stream cancel cannot interrupt a pull that is already in flight; a full fix needs an upstream napi cancel hook.)
124 followers Β· starred May 2025