@medicomind/rolldown-compression 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/dist/index.cjs +309 -0
- package/dist/index.d.cts +112 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.mjs +284 -0
- package/index.d.ts +56 -0
- package/index.js +590 -0
- package/package.json +95 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @medicomind/rolldown-compression
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`8c73f0c`](https://github.com/Medico-Mind/rolldown-compression/commit/8c73f0c725501fcf69ebbe3ea9a2d5264437d089) Thanks [@Mnwa](https://github.com/Mnwa)! - Update dependencies: TypeScript 7 toolchain, @types/node 26, zlib-rs 0.6.6.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [`798219c`](https://github.com/Medico-Mind/rolldown-compression/commit/798219c059dc6c514912bbaea3844e3c0d09b447) Thanks [@Mnwa](https://github.com/Mnwa)! - Elide lifetimes
|
|
12
|
+
|
|
13
|
+
- [`996537c`](https://github.com/Medico-Mind/rolldown-compression/commit/996537c2948681e323aa0d67fdc077e5e2887df2) Thanks [@Mnwa](https://github.com/Mnwa)! - rollback ts to 7
|
|
14
|
+
|
|
15
|
+
## 0.1.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [`5307247`](https://github.com/Medico-Mind/rolldown-compression/commit/5307247e332a479672b6f91bfa76953922bd695e) Thanks [@Mnwa](https://github.com/Mnwa)! - Initial release.
|
|
20
|
+
|
|
21
|
+
- Rolldown plugin compressing emitted assets with gzip, brotli and zstd.
|
|
22
|
+
- Native Rust compression core (napi-rs v3 + rayon): one batched FFI call per
|
|
23
|
+
build, parallel across files and algorithms, cost-aware scheduling,
|
|
24
|
+
off-main-thread execution.
|
|
25
|
+
- `compression()` / `defineAlgorithm()` API mirroring vite-plugin-compression2:
|
|
26
|
+
include/exclude filters, threshold, filename patterns, deleteOriginalAssets,
|
|
27
|
+
skipIfLargerOrEqual, concurrency, logLevel.
|
|
28
|
+
- Eager option validation, re-compression guard, watch-mode no-op with
|
|
29
|
+
`enableInWatchMode` opt-in.
|
|
30
|
+
- Prebuilt binaries for darwin-arm64/x64, linux-x64/arm64-gnu, linux-x64-musl,
|
|
31
|
+
win32-x64-msvc.
|
|
32
|
+
|
|
33
|
+
- [`66182be`](https://github.com/Medico-Mind/rolldown-compression/commit/66182be208983bbbcac805cf0bb1d2671f86fcd2) Thanks [@Mnwa](https://github.com/Mnwa)! - First release
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- [`0fdd849`](https://github.com/Medico-Mind/rolldown-compression/commit/0fdd849328e71701b1aedadb677a82e6e75640a8) Thanks [@Mnwa](https://github.com/Mnwa)! - Set repo
|
|
38
|
+
|
|
39
|
+
Release notes are generated by [changesets](https://github.com/changesets/changesets); new sections are added here automatically when a release PR is merged.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 medicomind
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @medicomind/rolldown-compression
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Medico-Mind/rolldown-compression/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@medicomind/rolldown-compression)
|
|
5
|
+
[](https://www.npmjs.com/package/@medicomind/rolldown-compression)
|
|
6
|
+
[](https://www.npmjs.com/package/@medicomind/rolldown-compression)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
Fast, native compression plugin for [Rolldown](https://rolldown.rs): compresses emitted assets with **gzip**, **brotli** and **zstd** at build time. The compression core is written in Rust (napi-rs + rayon) — one batched FFI call per build, fanned out across all CPU cores, without ever blocking the JS event loop.
|
|
10
|
+
|
|
11
|
+
API ergonomics mirror [`vite-plugin-compression2`](https://github.com/nonzzz/vite-plugin-compression); see [differences](#differences-from-vite-plugin-compression2).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install -D @medicomind/rolldown-compression
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Prebuilt binaries are installed automatically — no Rust toolchain needed (see [platform support](#platform-support)).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// rolldown.config.ts
|
|
25
|
+
import { defineConfig } from 'rolldown'
|
|
26
|
+
import { compression } from '@medicomind/rolldown-compression'
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
input: 'src/main.ts',
|
|
30
|
+
plugins: [
|
|
31
|
+
// gzip + brotli with defaults
|
|
32
|
+
compression(),
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Full configuration:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { compression, defineAlgorithm } from '@medicomind/rolldown-compression'
|
|
41
|
+
|
|
42
|
+
compression({
|
|
43
|
+
include: [/\.(js|mjs|css|html|svg|json|wasm)$/],
|
|
44
|
+
exclude: [/\.(png|jpe?g|webp|woff2?)$/],
|
|
45
|
+
threshold: 1024,
|
|
46
|
+
algorithms: [
|
|
47
|
+
'gzip', // string shorthand with default level
|
|
48
|
+
defineAlgorithm('brotli', { quality: 11 }),
|
|
49
|
+
defineAlgorithm('zstd', { level: 19 }),
|
|
50
|
+
],
|
|
51
|
+
filename: '[path][base].gz', // or (fileName, algorithm) => string
|
|
52
|
+
deleteOriginalAssets: false,
|
|
53
|
+
skipIfLargerOrEqual: true,
|
|
54
|
+
concurrency: 0, // 0 = number of logical CPUs
|
|
55
|
+
logLevel: 'info',
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
| option | type | default | description |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `include` | `string \| RegExp \| Array<string \| RegExp>` | `/\.(html\|xml\|css\|json\|js\|mjs\|svg\|yaml\|yml\|toml\|txt\|wasm)$/` | Files to compress. Strings are [picomatch](https://github.com/micromatch/picomatch) globs, matched against bundle-relative file names ([`@rollup/pluginutils` `createFilter`](https://github.com/rollup/plugins/tree/master/packages/pluginutils#createfilter) semantics). |
|
|
64
|
+
| `exclude` | `string \| RegExp \| Array<string \| RegExp>` | — | Files to skip. **Wins over `include`.** |
|
|
65
|
+
| `threshold` | `number` | `0` | Minimum original size in bytes for a file to be compressed. |
|
|
66
|
+
| `algorithms` | `Array<AlgorithmName \| DefineAlgorithmResult>` | `['gzip', 'brotli']` | Algorithms to run. Aliases `gz`, `br`, `brotliCompress`, `zstandard` normalize to `gzip` / `brotli` / `zstd`. |
|
|
67
|
+
| `filename` | `string \| (fileName, algorithm) => string` | `'[path][base]' + ext` | Name of the emitted artifact. Tokens: `[path]` (directory incl. trailing `/`), `[base]`, `[name]`, `[ext]` (with dot), `[hash]` (8-char sha256 of the content). Default extensions: `.gz`, `.br`, `.zst`. |
|
|
68
|
+
| `deleteOriginalAssets` | `boolean` | `false` | Remove the original from the bundle after all algorithms processed it. Errors if `filename` resolves to the source name. |
|
|
69
|
+
| `skipIfLargerOrEqual` | `boolean` | `true` | Don't emit artifacts whose compressed size is `>=` the original. |
|
|
70
|
+
| `concurrency` | `number` | `0` | Native worker threads. `0` = number of logical CPUs. |
|
|
71
|
+
| `logLevel` | `'silent' \| 'error' \| 'warn' \| 'info'` | `'info'` | Plugin log verbosity; `info` prints a per-algorithm summary at build end. |
|
|
72
|
+
| `enableInWatchMode` | `boolean` | `false` | The plugin is a no-op in watch/dev mode unless enabled (see [watch mode](#watch--dev-mode)). |
|
|
73
|
+
|
|
74
|
+
All options are validated when `compression()` is called — invalid levels (e.g. brotli quality 12), unknown algorithm names or malformed filters throw immediately, not mid-build.
|
|
75
|
+
|
|
76
|
+
### Per-algorithm options
|
|
77
|
+
|
|
78
|
+
| algorithm | option | range | default |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| `gzip` | `level` | 0–9 | 6 |
|
|
81
|
+
| `brotli` | `quality` | 0–11 | 11 |
|
|
82
|
+
| `brotli` | `windowBits` | 10–24 | 22 |
|
|
83
|
+
| `zstd` | `level` | 1–22 | 19 |
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
defineAlgorithm('gzip', { level: 9 })
|
|
87
|
+
defineAlgorithm('brotli', { quality: 7, windowBits: 22 })
|
|
88
|
+
defineAlgorithm('zstd', { level: 12 })
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How it works
|
|
92
|
+
|
|
93
|
+
- The plugin hooks **`generateBundle`**, while all chunks and assets are still in memory — no filesystem round-trip. Eligible files (filter + threshold) are sent to the native module as **one batched FFI call per build**; results are emitted with `emitFile`.
|
|
94
|
+
- Compression runs on a rayon thread pool inside the native module (`AsyncTask`), parallel across files *and* algorithms, scheduled most-expensive-first so one large brotli file can't stretch the batch tail. The JS event loop keeps ticking throughout (covered by a test).
|
|
95
|
+
- Buffers cross the FFI boundary without base64/string round-trips, and the compression working set lives in native memory — a 500 MB asset does not pressure the JS heap.
|
|
96
|
+
- A failing task never aborts the batch: per-task errors are aggregated and fail the build with one message.
|
|
97
|
+
- Already-compressed artifacts (`.gz`, `.br`, `.zst` — ours or pre-existing) are never re-compressed, so chaining plugin instances can't produce `app.js.gz.br`.
|
|
98
|
+
|
|
99
|
+
Limitation: assets written to disk by other plugins in `writeBundle`/`closeBundle` (i.e. after `generateBundle`) are not seen by this plugin. This matches how `vite-plugin-compression2` handles the in-bundle pass; a `writeBundle` fallback was deliberately left out to keep the pipeline zero-copy and single-pass. File an issue if you have a concrete case.
|
|
100
|
+
|
|
101
|
+
### Watch / dev mode
|
|
102
|
+
|
|
103
|
+
The plugin declares `apply: 'build'` (honored by Vite-style hosts such as `rolldown-vite`) **and** checks `this.meta.watchMode` at `generateBundle` time, making it a no-op under `rolldown --watch`. Set `enableInWatchMode: true` to compress in watch builds anyway.
|
|
104
|
+
|
|
105
|
+
## Serving pre-compressed assets
|
|
106
|
+
|
|
107
|
+
nginx ([gzip_static](https://nginx.org/en/docs/http/ngx_http_gzip_static_module.html) / [brotli_static](https://github.com/google/ngx_brotli) / [zstd_static](https://github.com/tokers/zstd-nginx-module)):
|
|
108
|
+
|
|
109
|
+
```nginx
|
|
110
|
+
location / {
|
|
111
|
+
gzip_static on; # serves foo.js.gz when the client accepts gzip
|
|
112
|
+
brotli_static on; # requires ngx_brotli
|
|
113
|
+
zstd_static on; # requires zstd-nginx-module
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Caddy:
|
|
118
|
+
|
|
119
|
+
```caddyfile
|
|
120
|
+
example.com {
|
|
121
|
+
root * /srv/dist
|
|
122
|
+
file_server {
|
|
123
|
+
precompressed zstd br gzip
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Benchmark
|
|
129
|
+
|
|
130
|
+
`npm run bench` (or `node benchmark/index.mjs --quick`) generates a dist-shaped fixture set — 200 files / ~48 MB with a long-tail size distribution — and compresses it with the native core vs `node:zlib` driven at full parallelism via `Promise.all` (the reference plugin's best case). Both sides always use the same levels.
|
|
131
|
+
|
|
132
|
+
Results on an Apple M1 Pro (10 cores), Node 26, default `UV_THREADPOOL_SIZE`:
|
|
133
|
+
|
|
134
|
+
| scenario | output | native (rust) | node:zlib | speedup |
|
|
135
|
+
| --- | --- | --- | --- | --- |
|
|
136
|
+
| gzip+brotli (ref. defaults: 9/11) | 8.71 MB | 8.82s | 15.19s | **1.72x** |
|
|
137
|
+
| gzip (level 9) | 5.51 MB | 0.10s | 0.33s | **3.48x** |
|
|
138
|
+
| gzip (level 6) | 5.64 MB | 0.07s | 0.16s | **2.38x** |
|
|
139
|
+
| brotli (quality 11) | 3.21 MB | 8.16s | 14.96s | **1.83x** |
|
|
140
|
+
| zstd (level 19) | 3.28 MB | 2.19s | 6.91s | **3.15x** |
|
|
141
|
+
|
|
142
|
+
Reading these numbers honestly:
|
|
143
|
+
|
|
144
|
+
- **gzip** and **zstd** beat the ≥3x target: the Rust encoders ([zlib-rs](https://github.com/trifectatechfoundation/zlib-rs), ~2.4x faster per core than node's bundled zlib in our measurements; libzstd) are faster per core *and* use every core, while `node:zlib` is capped at `UV_THREADPOOL_SIZE` (default 4) threads.
|
|
145
|
+
- **brotli at quality 11** is the bound on the combined number: the Rust `brotli` crate is at per-core parity with node's C brotli (we measured a 1.01 single-thread ratio), so the achievable speedup is roughly `cores / UV_THREADPOOL_SIZE` — ~2x on 8 equal cores, more on bigger machines. No implementation can honestly do better without changing the algorithm or its level.
|
|
146
|
+
- The speedup grows with core count and shrinks if you raise `UV_THREADPOOL_SIZE` for the JS side — the benchmark prints both so runs are comparable.
|
|
147
|
+
|
|
148
|
+
## Platform support
|
|
149
|
+
|
|
150
|
+
Prebuilt binaries are published for:
|
|
151
|
+
|
|
152
|
+
| platform | triple |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| macOS arm64 | `aarch64-apple-darwin` |
|
|
155
|
+
| macOS x64 | `x86_64-apple-darwin` |
|
|
156
|
+
| Linux x64 (glibc) | `x86_64-unknown-linux-gnu` |
|
|
157
|
+
| Linux arm64 (glibc) | `aarch64-unknown-linux-gnu` |
|
|
158
|
+
| Linux x64 (musl) | `x86_64-unknown-linux-musl` |
|
|
159
|
+
| Windows x64 | `x86_64-pc-windows-msvc` |
|
|
160
|
+
|
|
161
|
+
Node.js >= 18.
|
|
162
|
+
|
|
163
|
+
## Differences from vite-plugin-compression2
|
|
164
|
+
|
|
165
|
+
- **Native speed**: compression runs in Rust on all cores, one FFI batch per build, instead of `node:zlib` calls through the libuv thread pool.
|
|
166
|
+
- **No custom JS algorithms**: `algorithms` accepts only the built-in `gzip` / `brotli` / `zstd` (function-form algorithms can't cross the FFI boundary). `defineAlgorithm` returns an opaque object, not a `[name, options]` tuple — treat it as such.
|
|
167
|
+
- **No tarball plugin**: out of scope.
|
|
168
|
+
- **gzip default level is 6** (zlib default), not 9 — measurably faster for a ~1% size difference. Pass `defineAlgorithm('gzip', { level: 9 })` to match the reference.
|
|
169
|
+
- **zstd everywhere**: zstd is compiled in, with no dependency on the Node runtime's zstd support (node >= 22.15).
|
|
170
|
+
- Extra options: `concurrency` (native thread cap) and `enableInWatchMode`.
|
|
171
|
+
|
|
172
|
+
## Implementation decisions
|
|
173
|
+
|
|
174
|
+
- **Rolldown target**: developed and tested against `rolldown@1.1.x` (peer range `^1.0.0`), using the Rollup-compatible `generateBundle`/`emitFile` plugin API.
|
|
175
|
+
- **gzip backend**: `flate2` with the pure-Rust `zlib-rs` backend — as fast as or faster than zlib-ng in our runs, with no cmake/C toolchain requirement for contributors.
|
|
176
|
+
- **Publishing**: public npm (`--access public`), versioned with [changesets](https://github.com/changesets/changesets). PRs include a changeset (`npx changeset`); the Version workflow keeps a `chore: release` PR up to date, and merging it tags the release and runs the full napi build matrix before `napi prepublish` + `npm publish`. Run the Release workflow via `workflow_dispatch` for a dry-run that builds all platform artifacts without publishing.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full contributor guide (setup, tests, changesets, PR workflow).
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
npm install # install JS deps
|
|
184
|
+
npm run build # release native build + TS bundle
|
|
185
|
+
npm test # vitest (unit + integration)
|
|
186
|
+
cargo test # Rust core tests
|
|
187
|
+
npm run bench # benchmark vs node:zlib
|
|
188
|
+
COMPRESSION_TEST_LARGE=1 npx vitest run __tests__/integration/large-file.test.ts # 150 MB asset test
|
|
189
|
+
npx changeset # add a changeset describing your change (required for releases)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_crypto = require("node:crypto");
|
|
25
|
+
let node_path = require("node:path");
|
|
26
|
+
node_path = __toESM(node_path);
|
|
27
|
+
let _rollup_pluginutils = require("@rollup/pluginutils");
|
|
28
|
+
let node_module = require("node:module");
|
|
29
|
+
//#region ts/options.ts
|
|
30
|
+
/**
|
|
31
|
+
* Option types, normalization and validation for the public API.
|
|
32
|
+
*
|
|
33
|
+
* Everything here is validated eagerly at `compression()` call time so that
|
|
34
|
+
* misconfiguration fails when the config is evaluated, not mid-build.
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_INCLUDE = /\.(html|xml|css|json|js|mjs|svg|yaml|yml|toml|txt|wasm)$/;
|
|
37
|
+
const ALIASES = {
|
|
38
|
+
gzip: "gzip",
|
|
39
|
+
gz: "gzip",
|
|
40
|
+
brotli: "brotli",
|
|
41
|
+
br: "brotli",
|
|
42
|
+
brotliCompress: "brotli",
|
|
43
|
+
zstd: "zstd",
|
|
44
|
+
zstandard: "zstd"
|
|
45
|
+
};
|
|
46
|
+
const EXTENSIONS = {
|
|
47
|
+
gzip: ".gz",
|
|
48
|
+
brotli: ".br",
|
|
49
|
+
zstd: ".zst"
|
|
50
|
+
};
|
|
51
|
+
const DEFAULT_LEVELS = {
|
|
52
|
+
gzip: 6,
|
|
53
|
+
brotli: 11,
|
|
54
|
+
zstd: 19
|
|
55
|
+
};
|
|
56
|
+
const LEVEL_RANGES = {
|
|
57
|
+
gzip: [0, 9],
|
|
58
|
+
brotli: [0, 11],
|
|
59
|
+
zstd: [1, 22]
|
|
60
|
+
};
|
|
61
|
+
const LOG_LEVELS = [
|
|
62
|
+
"silent",
|
|
63
|
+
"error",
|
|
64
|
+
"warn",
|
|
65
|
+
"info"
|
|
66
|
+
];
|
|
67
|
+
/** File extensions produced by this plugin; used by the re-compression guard. */
|
|
68
|
+
const COMPRESSED_EXTENSION_RE = /\.(gz|br|zst)$/i;
|
|
69
|
+
var OptionValidationError = class extends Error {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(`[rolldown-compression] ${message}`);
|
|
72
|
+
this.name = "OptionValidationError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function assertIntegerInRange(value, [min, max], label) {
|
|
76
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) throw new OptionValidationError(`invalid ${label}: expected an integer between ${min} and ${max}, got ${JSON.stringify(value)}`);
|
|
77
|
+
}
|
|
78
|
+
/** Normalize an algorithm name, throwing on unknown input. */
|
|
79
|
+
function normalizeAlgorithmName(name) {
|
|
80
|
+
const canonical = ALIASES[name];
|
|
81
|
+
if (canonical === void 0) throw new OptionValidationError(`unknown algorithm ${JSON.stringify(name)}: expected one of ${Object.keys(ALIASES).join(", ")}`);
|
|
82
|
+
return canonical;
|
|
83
|
+
}
|
|
84
|
+
function defineAlgorithm(name, options = {}) {
|
|
85
|
+
const algorithm = normalizeAlgorithmName(name);
|
|
86
|
+
validateAlgorithmOptions(algorithm, options);
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
algorithm,
|
|
89
|
+
options: Object.freeze({ ...options })
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function validateAlgorithmOptions(algorithm, options) {
|
|
93
|
+
if (algorithm === "brotli") {
|
|
94
|
+
const { quality, windowBits } = options;
|
|
95
|
+
if (quality !== void 0) assertIntegerInRange(quality, LEVEL_RANGES.brotli, "brotli quality");
|
|
96
|
+
if (windowBits !== void 0) assertIntegerInRange(windowBits, [10, 24], "brotli windowBits");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { level } = options;
|
|
100
|
+
if (level !== void 0) assertIntegerInRange(level, LEVEL_RANGES[algorithm], `${algorithm} level`);
|
|
101
|
+
}
|
|
102
|
+
function resolveAlgorithm(entry) {
|
|
103
|
+
const { algorithm, options } = typeof entry === "string" ? {
|
|
104
|
+
algorithm: normalizeAlgorithmName(entry),
|
|
105
|
+
options: {}
|
|
106
|
+
} : validateDefined(entry);
|
|
107
|
+
const resolved = {
|
|
108
|
+
algorithm,
|
|
109
|
+
level: algorithm === "brotli" ? options.quality ?? DEFAULT_LEVELS.brotli : options.level ?? DEFAULT_LEVELS[algorithm],
|
|
110
|
+
extension: EXTENSIONS[algorithm]
|
|
111
|
+
};
|
|
112
|
+
if (algorithm === "brotli") {
|
|
113
|
+
const { windowBits } = options;
|
|
114
|
+
if (windowBits !== void 0) resolved.windowBits = windowBits;
|
|
115
|
+
}
|
|
116
|
+
return resolved;
|
|
117
|
+
}
|
|
118
|
+
function validateDefined(entry) {
|
|
119
|
+
if (entry === null || typeof entry !== "object" || typeof entry.algorithm !== "string" || entry.options === null || typeof entry.options !== "object") throw new OptionValidationError(`invalid algorithms entry ${JSON.stringify(entry)}: expected an algorithm name or a defineAlgorithm() result`);
|
|
120
|
+
const algorithm = normalizeAlgorithmName(entry.algorithm);
|
|
121
|
+
validateAlgorithmOptions(algorithm, entry.options);
|
|
122
|
+
return {
|
|
123
|
+
algorithm,
|
|
124
|
+
options: entry.options
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Validate and normalize user options. Throws at `compression()` call time. */
|
|
128
|
+
function resolveOptions(options = {}) {
|
|
129
|
+
if (options === null || typeof options !== "object") throw new OptionValidationError("options must be an object");
|
|
130
|
+
const { include = DEFAULT_INCLUDE, exclude, threshold = 0, algorithms = ["gzip", "brotli"], filename, deleteOriginalAssets = false, skipIfLargerOrEqual = true, concurrency = 0, logLevel = "info", enableInWatchMode = false } = options;
|
|
131
|
+
if (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0) throw new OptionValidationError(`invalid threshold: expected a non-negative number, got ${JSON.stringify(threshold)}`);
|
|
132
|
+
if (!Number.isInteger(concurrency) || concurrency < 0) throw new OptionValidationError(`invalid concurrency: expected a non-negative integer, got ${JSON.stringify(concurrency)}`);
|
|
133
|
+
if (!LOG_LEVELS.includes(logLevel)) throw new OptionValidationError(`invalid logLevel ${JSON.stringify(logLevel)}: expected one of ${LOG_LEVELS.join(", ")}`);
|
|
134
|
+
if (filename !== void 0 && typeof filename !== "string" && typeof filename !== "function") throw new OptionValidationError("invalid filename: expected a string pattern or a function");
|
|
135
|
+
if (!Array.isArray(algorithms) || algorithms.length === 0) throw new OptionValidationError("invalid algorithms: expected a non-empty array");
|
|
136
|
+
return {
|
|
137
|
+
filter: (0, _rollup_pluginutils.createFilter)(include, exclude, { resolve: false }),
|
|
138
|
+
threshold,
|
|
139
|
+
algorithms: algorithms.map(resolveAlgorithm),
|
|
140
|
+
filename,
|
|
141
|
+
deleteOriginalAssets: Boolean(deleteOriginalAssets),
|
|
142
|
+
skipIfLargerOrEqual: Boolean(skipIfLargerOrEqual),
|
|
143
|
+
concurrency,
|
|
144
|
+
logLevel,
|
|
145
|
+
enableInWatchMode: Boolean(enableInWatchMode)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve the emitted file name for a compressed artifact.
|
|
150
|
+
*
|
|
151
|
+
* Supported tokens: `[path]`, `[base]`, `[name]`, `[ext]`, `[hash]`.
|
|
152
|
+
*/
|
|
153
|
+
function resolveOutputFileName(filename, fileName, algorithm, source) {
|
|
154
|
+
if (typeof filename === "function") return filename(fileName, algorithm.algorithm);
|
|
155
|
+
const pattern = filename ?? `[path][base]${algorithm.extension}`;
|
|
156
|
+
const dir = node_path.default.posix.dirname(fileName);
|
|
157
|
+
const base = node_path.default.posix.basename(fileName);
|
|
158
|
+
const ext = node_path.default.posix.extname(fileName);
|
|
159
|
+
return pattern.replaceAll("[path]", dir === "." ? "" : `${dir}/`).replaceAll("[base]", base).replaceAll("[name]", base.slice(0, base.length - ext.length)).replaceAll("[ext]", ext).replaceAll("[hash]", () => (0, node_crypto.createHash)("sha256").update(source).digest("hex").slice(0, 8));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Compress a batch of buffers in the native rayon thread pool, off the JS
|
|
163
|
+
* main thread. `tasks[i]` describes how to compress `buffers[i]`.
|
|
164
|
+
*/
|
|
165
|
+
const compressBuffers = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../index.js").compressBuffers;
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region ts/plugin.ts
|
|
168
|
+
const PLUGIN_NAME = "rolldown-compression";
|
|
169
|
+
const LOG_PRIORITY = {
|
|
170
|
+
silent: 0,
|
|
171
|
+
error: 1,
|
|
172
|
+
warn: 2,
|
|
173
|
+
info: 3
|
|
174
|
+
};
|
|
175
|
+
function createLogger(level) {
|
|
176
|
+
const priority = LOG_PRIORITY[level];
|
|
177
|
+
const prefix = `[${PLUGIN_NAME}]`;
|
|
178
|
+
return {
|
|
179
|
+
info: (message) => {
|
|
180
|
+
if (priority >= LOG_PRIORITY.info) console.info(`${prefix} ${message}`);
|
|
181
|
+
},
|
|
182
|
+
warn: (message) => {
|
|
183
|
+
if (priority >= LOG_PRIORITY.warn) console.warn(`${prefix} ${message}`);
|
|
184
|
+
},
|
|
185
|
+
error: (message) => {
|
|
186
|
+
if (priority >= LOG_PRIORITY.error) console.error(`${prefix} ${message}`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function toBuffer(source) {
|
|
191
|
+
return typeof source === "string" ? Buffer.from(source, "utf8") : Buffer.from(source);
|
|
192
|
+
}
|
|
193
|
+
function formatBytes(bytes) {
|
|
194
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
195
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
196
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
197
|
+
}
|
|
198
|
+
function createCompressionPlugin(options) {
|
|
199
|
+
const logger = createLogger(options.logLevel);
|
|
200
|
+
const emittedNames = /* @__PURE__ */ new Set();
|
|
201
|
+
return Object.assign({
|
|
202
|
+
name: PLUGIN_NAME,
|
|
203
|
+
async generateBundle(_outputOptions, bundle) {
|
|
204
|
+
if (this.meta?.watchMode && !options.enableInWatchMode) {
|
|
205
|
+
logger.info("watch mode detected, skipping compression (set enableInWatchMode to opt in)");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const startedAt = performance.now();
|
|
209
|
+
const pending = [];
|
|
210
|
+
const processedSources = /* @__PURE__ */ new Set();
|
|
211
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
212
|
+
if (!options.filter(fileName)) continue;
|
|
213
|
+
if (COMPRESSED_EXTENSION_RE.test(fileName) || emittedNames.has(fileName)) continue;
|
|
214
|
+
const buffer = toBuffer(output.type === "chunk" ? output.code : output.source);
|
|
215
|
+
if (buffer.byteLength < options.threshold) continue;
|
|
216
|
+
for (const algorithm of options.algorithms) {
|
|
217
|
+
const outputFileName = resolveOutputFileName(options.filename, fileName, algorithm, buffer);
|
|
218
|
+
if (outputFileName === fileName) this.error(/* @__PURE__ */ new Error(`[${PLUGIN_NAME}] the filename option resolved "${fileName}" (${algorithm.algorithm}) to the same name as the source asset; refusing to overwrite it`));
|
|
219
|
+
pending.push({
|
|
220
|
+
task: {
|
|
221
|
+
fileName,
|
|
222
|
+
algorithm: algorithm.algorithm,
|
|
223
|
+
level: algorithm.level,
|
|
224
|
+
windowBits: algorithm.windowBits
|
|
225
|
+
},
|
|
226
|
+
buffer,
|
|
227
|
+
sourceFileName: fileName,
|
|
228
|
+
outputFileName
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
processedSources.add(fileName);
|
|
232
|
+
}
|
|
233
|
+
if (pending.length === 0) return;
|
|
234
|
+
const results = await compressBuffers(pending.map((artifact) => artifact.task), pending.map((artifact) => artifact.buffer), {
|
|
235
|
+
concurrency: options.concurrency,
|
|
236
|
+
skipIfLargerOrEqual: options.skipIfLargerOrEqual
|
|
237
|
+
});
|
|
238
|
+
const failures = [];
|
|
239
|
+
const emittedBySource = /* @__PURE__ */ new Map();
|
|
240
|
+
const stats = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const [index, result] of results.entries()) {
|
|
242
|
+
const artifact = pending[index];
|
|
243
|
+
if (artifact === void 0) continue;
|
|
244
|
+
if (result.error !== void 0 && result.error !== null) {
|
|
245
|
+
failures.push(`${result.fileName} (${result.algorithm}): ${result.error}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (result.skipped) {
|
|
249
|
+
logger.info(`skipped ${artifact.outputFileName}: ${result.algorithm} output would not be smaller than the original`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
this.emitFile({
|
|
253
|
+
type: "asset",
|
|
254
|
+
fileName: artifact.outputFileName,
|
|
255
|
+
source: result.data
|
|
256
|
+
});
|
|
257
|
+
emittedNames.add(artifact.outputFileName);
|
|
258
|
+
emittedBySource.set(artifact.sourceFileName, (emittedBySource.get(artifact.sourceFileName) ?? 0) + 1);
|
|
259
|
+
const stat = stats.get(result.algorithm) ?? {
|
|
260
|
+
count: 0,
|
|
261
|
+
originalBytes: 0,
|
|
262
|
+
outputBytes: 0
|
|
263
|
+
};
|
|
264
|
+
stat.count += 1;
|
|
265
|
+
stat.originalBytes += result.originalSize;
|
|
266
|
+
stat.outputBytes += result.compressedSize;
|
|
267
|
+
stats.set(result.algorithm, stat);
|
|
268
|
+
}
|
|
269
|
+
if (failures.length > 0) this.error(/* @__PURE__ */ new Error(`[${PLUGIN_NAME}] ${failures.length} compression task(s) failed:\n${failures.join("\n")}`));
|
|
270
|
+
if (options.deleteOriginalAssets) for (const fileName of processedSources) {
|
|
271
|
+
if ((emittedBySource.get(fileName) ?? 0) === 0) logger.warn(`deleteOriginalAssets removed "${fileName}" even though no compressed variant was emitted for it`);
|
|
272
|
+
delete bundle[fileName];
|
|
273
|
+
}
|
|
274
|
+
const elapsedMs = performance.now() - startedAt;
|
|
275
|
+
const summary = [...stats.entries()].map(([algorithm, stat]) => `${algorithm}: ${stat.count} file(s), ${formatBytes(stat.originalBytes)} -> ${formatBytes(stat.outputBytes)}`).join("; ");
|
|
276
|
+
if (summary.length > 0) {
|
|
277
|
+
const totalSaved = [...stats.values()].reduce((sum, stat) => sum + (stat.originalBytes - stat.outputBytes), 0);
|
|
278
|
+
logger.info(`${summary}; saved ${formatBytes(totalSaved)} in ${(elapsedMs / 1e3).toFixed(2)}s`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}, { apply: "build" });
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region ts/index.ts
|
|
285
|
+
/**
|
|
286
|
+
* Create the compression plugin.
|
|
287
|
+
*
|
|
288
|
+
* Options are validated eagerly: invalid levels, algorithm names, thresholds
|
|
289
|
+
* etc. throw here rather than during the build.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* import { defineConfig } from 'rolldown'
|
|
293
|
+
* import { compression, defineAlgorithm } from '@medicomind/rolldown-compression'
|
|
294
|
+
*
|
|
295
|
+
* export default defineConfig({
|
|
296
|
+
* plugins: [
|
|
297
|
+
* compression({
|
|
298
|
+
* threshold: 1024,
|
|
299
|
+
* algorithms: ['gzip', defineAlgorithm('brotli', { quality: 11 })],
|
|
300
|
+
* }),
|
|
301
|
+
* ],
|
|
302
|
+
* })
|
|
303
|
+
*/
|
|
304
|
+
function compression(options) {
|
|
305
|
+
return createCompressionPlugin(resolveOptions(options));
|
|
306
|
+
}
|
|
307
|
+
//#endregion
|
|
308
|
+
exports.compression = compression;
|
|
309
|
+
exports.defineAlgorithm = defineAlgorithm;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Plugin } from "rolldown";
|
|
2
|
+
//#region ts/options.d.ts
|
|
3
|
+
/** Accepted algorithm names, including aliases. */
|
|
4
|
+
type AlgorithmName = 'gzip' | 'gz' | 'brotli' | 'br' | 'brotliCompress' | 'zstd' | 'zstandard';
|
|
5
|
+
/** Canonical algorithm names after alias normalization. */
|
|
6
|
+
type CanonicalAlgorithm = 'gzip' | 'brotli' | 'zstd';
|
|
7
|
+
/** Gzip options. `level`: 0-9, default 6. */
|
|
8
|
+
interface GzipOptions {
|
|
9
|
+
level?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Brotli options. `quality`: 0-11, default 11. `windowBits`: 10-24, default 22. */
|
|
12
|
+
interface BrotliOptions {
|
|
13
|
+
quality?: number;
|
|
14
|
+
windowBits?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Zstandard options. `level`: 1-22, default 19. */
|
|
17
|
+
interface ZstdOptions {
|
|
18
|
+
level?: number;
|
|
19
|
+
}
|
|
20
|
+
/** A normalized `(algorithm, options)` pair produced by {@link defineAlgorithm}. */
|
|
21
|
+
interface DefineAlgorithmResult {
|
|
22
|
+
readonly algorithm: CanonicalAlgorithm;
|
|
23
|
+
readonly options: GzipOptions | BrotliOptions | ZstdOptions;
|
|
24
|
+
}
|
|
25
|
+
/** Pattern or callback used to derive compressed artifact names. */
|
|
26
|
+
type FilenameOption = string | ((fileName: string, algorithm: CanonicalAlgorithm) => string);
|
|
27
|
+
type LogLevel = 'silent' | 'error' | 'warn' | 'info';
|
|
28
|
+
/** Options accepted by {@link compression}. */
|
|
29
|
+
interface CompressionOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Files to compress. Strings are picomatch globs, RegExps are tested
|
|
32
|
+
* against the output file name.
|
|
33
|
+
* @default /\.(html|xml|css|json|js|mjs|svg|yaml|yml|toml|txt|wasm)$/
|
|
34
|
+
*/
|
|
35
|
+
include?: string | RegExp | Array<string | RegExp>;
|
|
36
|
+
/** Files to exclude. Takes precedence over `include`. */
|
|
37
|
+
exclude?: string | RegExp | Array<string | RegExp>;
|
|
38
|
+
/**
|
|
39
|
+
* Minimum size of the original asset, in bytes, for it to be compressed.
|
|
40
|
+
* @default 0
|
|
41
|
+
*/
|
|
42
|
+
threshold?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Algorithms to run, as names or {@link defineAlgorithm} results.
|
|
45
|
+
* @default ['gzip', 'brotli']
|
|
46
|
+
*/
|
|
47
|
+
algorithms?: Array<AlgorithmName | DefineAlgorithmResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Name of the emitted artifact. Tokens: `[path]` (directory, with trailing
|
|
50
|
+
* slash), `[base]` (file name with extension), `[name]`, `[ext]` (with
|
|
51
|
+
* leading dot), `[hash]` (8-char content hash). Function form receives the
|
|
52
|
+
* original file name and the canonical algorithm name.
|
|
53
|
+
* @default '[path][base]' + per-algorithm extension (.gz / .br / .zst)
|
|
54
|
+
*/
|
|
55
|
+
filename?: FilenameOption;
|
|
56
|
+
/**
|
|
57
|
+
* Remove the original asset from the bundle once all algorithms have
|
|
58
|
+
* processed it.
|
|
59
|
+
* @default false
|
|
60
|
+
*/
|
|
61
|
+
deleteOriginalAssets?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Do not emit artifacts whose compressed size is >= the original size.
|
|
64
|
+
* @default true
|
|
65
|
+
*/
|
|
66
|
+
skipIfLargerOrEqual?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Native worker threads used for compression. `0` = number of logical CPUs.
|
|
69
|
+
* @default 0
|
|
70
|
+
*/
|
|
71
|
+
concurrency?: number;
|
|
72
|
+
/** @default 'info' */
|
|
73
|
+
logLevel?: LogLevel;
|
|
74
|
+
/**
|
|
75
|
+
* The plugin is a no-op in watch/dev mode unless this is set to `true`.
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
enableInWatchMode?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Pair an algorithm with its options, validating both eagerly.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* compression({ algorithms: [defineAlgorithm('brotli', { quality: 9 })] })
|
|
85
|
+
*/
|
|
86
|
+
declare function defineAlgorithm(name: 'gzip' | 'gz', options?: GzipOptions): DefineAlgorithmResult;
|
|
87
|
+
declare function defineAlgorithm(name: 'brotli' | 'br' | 'brotliCompress', options?: BrotliOptions): DefineAlgorithmResult;
|
|
88
|
+
declare function defineAlgorithm(name: 'zstd' | 'zstandard', options?: ZstdOptions): DefineAlgorithmResult;
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region ts/index.d.ts
|
|
91
|
+
/**
|
|
92
|
+
* Create the compression plugin.
|
|
93
|
+
*
|
|
94
|
+
* Options are validated eagerly: invalid levels, algorithm names, thresholds
|
|
95
|
+
* etc. throw here rather than during the build.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* import { defineConfig } from 'rolldown'
|
|
99
|
+
* import { compression, defineAlgorithm } from '@medicomind/rolldown-compression'
|
|
100
|
+
*
|
|
101
|
+
* export default defineConfig({
|
|
102
|
+
* plugins: [
|
|
103
|
+
* compression({
|
|
104
|
+
* threshold: 1024,
|
|
105
|
+
* algorithms: ['gzip', defineAlgorithm('brotli', { quality: 11 })],
|
|
106
|
+
* }),
|
|
107
|
+
* ],
|
|
108
|
+
* })
|
|
109
|
+
*/
|
|
110
|
+
declare function compression(options?: CompressionOptions): Plugin;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { type AlgorithmName, type BrotliOptions, type CanonicalAlgorithm, type CompressionOptions, type DefineAlgorithmResult, type FilenameOption, type GzipOptions, type LogLevel, type ZstdOptions, compression, defineAlgorithm };
|