@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/dist/index.d.mts
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 };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createFilter } from "@rollup/pluginutils";
|
|
5
|
+
//#region ts/options.ts
|
|
6
|
+
/**
|
|
7
|
+
* Option types, normalization and validation for the public API.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is validated eagerly at `compression()` call time so that
|
|
10
|
+
* misconfiguration fails when the config is evaluated, not mid-build.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_INCLUDE = /\.(html|xml|css|json|js|mjs|svg|yaml|yml|toml|txt|wasm)$/;
|
|
13
|
+
const ALIASES = {
|
|
14
|
+
gzip: "gzip",
|
|
15
|
+
gz: "gzip",
|
|
16
|
+
brotli: "brotli",
|
|
17
|
+
br: "brotli",
|
|
18
|
+
brotliCompress: "brotli",
|
|
19
|
+
zstd: "zstd",
|
|
20
|
+
zstandard: "zstd"
|
|
21
|
+
};
|
|
22
|
+
const EXTENSIONS = {
|
|
23
|
+
gzip: ".gz",
|
|
24
|
+
brotli: ".br",
|
|
25
|
+
zstd: ".zst"
|
|
26
|
+
};
|
|
27
|
+
const DEFAULT_LEVELS = {
|
|
28
|
+
gzip: 6,
|
|
29
|
+
brotli: 11,
|
|
30
|
+
zstd: 19
|
|
31
|
+
};
|
|
32
|
+
const LEVEL_RANGES = {
|
|
33
|
+
gzip: [0, 9],
|
|
34
|
+
brotli: [0, 11],
|
|
35
|
+
zstd: [1, 22]
|
|
36
|
+
};
|
|
37
|
+
const LOG_LEVELS = [
|
|
38
|
+
"silent",
|
|
39
|
+
"error",
|
|
40
|
+
"warn",
|
|
41
|
+
"info"
|
|
42
|
+
];
|
|
43
|
+
/** File extensions produced by this plugin; used by the re-compression guard. */
|
|
44
|
+
const COMPRESSED_EXTENSION_RE = /\.(gz|br|zst)$/i;
|
|
45
|
+
var OptionValidationError = class extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(`[rolldown-compression] ${message}`);
|
|
48
|
+
this.name = "OptionValidationError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function assertIntegerInRange(value, [min, max], label) {
|
|
52
|
+
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)}`);
|
|
53
|
+
}
|
|
54
|
+
/** Normalize an algorithm name, throwing on unknown input. */
|
|
55
|
+
function normalizeAlgorithmName(name) {
|
|
56
|
+
const canonical = ALIASES[name];
|
|
57
|
+
if (canonical === void 0) throw new OptionValidationError(`unknown algorithm ${JSON.stringify(name)}: expected one of ${Object.keys(ALIASES).join(", ")}`);
|
|
58
|
+
return canonical;
|
|
59
|
+
}
|
|
60
|
+
function defineAlgorithm(name, options = {}) {
|
|
61
|
+
const algorithm = normalizeAlgorithmName(name);
|
|
62
|
+
validateAlgorithmOptions(algorithm, options);
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
algorithm,
|
|
65
|
+
options: Object.freeze({ ...options })
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function validateAlgorithmOptions(algorithm, options) {
|
|
69
|
+
if (algorithm === "brotli") {
|
|
70
|
+
const { quality, windowBits } = options;
|
|
71
|
+
if (quality !== void 0) assertIntegerInRange(quality, LEVEL_RANGES.brotli, "brotli quality");
|
|
72
|
+
if (windowBits !== void 0) assertIntegerInRange(windowBits, [10, 24], "brotli windowBits");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const { level } = options;
|
|
76
|
+
if (level !== void 0) assertIntegerInRange(level, LEVEL_RANGES[algorithm], `${algorithm} level`);
|
|
77
|
+
}
|
|
78
|
+
function resolveAlgorithm(entry) {
|
|
79
|
+
const { algorithm, options } = typeof entry === "string" ? {
|
|
80
|
+
algorithm: normalizeAlgorithmName(entry),
|
|
81
|
+
options: {}
|
|
82
|
+
} : validateDefined(entry);
|
|
83
|
+
const resolved = {
|
|
84
|
+
algorithm,
|
|
85
|
+
level: algorithm === "brotli" ? options.quality ?? DEFAULT_LEVELS.brotli : options.level ?? DEFAULT_LEVELS[algorithm],
|
|
86
|
+
extension: EXTENSIONS[algorithm]
|
|
87
|
+
};
|
|
88
|
+
if (algorithm === "brotli") {
|
|
89
|
+
const { windowBits } = options;
|
|
90
|
+
if (windowBits !== void 0) resolved.windowBits = windowBits;
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
function validateDefined(entry) {
|
|
95
|
+
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`);
|
|
96
|
+
const algorithm = normalizeAlgorithmName(entry.algorithm);
|
|
97
|
+
validateAlgorithmOptions(algorithm, entry.options);
|
|
98
|
+
return {
|
|
99
|
+
algorithm,
|
|
100
|
+
options: entry.options
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Validate and normalize user options. Throws at `compression()` call time. */
|
|
104
|
+
function resolveOptions(options = {}) {
|
|
105
|
+
if (options === null || typeof options !== "object") throw new OptionValidationError("options must be an object");
|
|
106
|
+
const { include = DEFAULT_INCLUDE, exclude, threshold = 0, algorithms = ["gzip", "brotli"], filename, deleteOriginalAssets = false, skipIfLargerOrEqual = true, concurrency = 0, logLevel = "info", enableInWatchMode = false } = options;
|
|
107
|
+
if (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0) throw new OptionValidationError(`invalid threshold: expected a non-negative number, got ${JSON.stringify(threshold)}`);
|
|
108
|
+
if (!Number.isInteger(concurrency) || concurrency < 0) throw new OptionValidationError(`invalid concurrency: expected a non-negative integer, got ${JSON.stringify(concurrency)}`);
|
|
109
|
+
if (!LOG_LEVELS.includes(logLevel)) throw new OptionValidationError(`invalid logLevel ${JSON.stringify(logLevel)}: expected one of ${LOG_LEVELS.join(", ")}`);
|
|
110
|
+
if (filename !== void 0 && typeof filename !== "string" && typeof filename !== "function") throw new OptionValidationError("invalid filename: expected a string pattern or a function");
|
|
111
|
+
if (!Array.isArray(algorithms) || algorithms.length === 0) throw new OptionValidationError("invalid algorithms: expected a non-empty array");
|
|
112
|
+
return {
|
|
113
|
+
filter: createFilter(include, exclude, { resolve: false }),
|
|
114
|
+
threshold,
|
|
115
|
+
algorithms: algorithms.map(resolveAlgorithm),
|
|
116
|
+
filename,
|
|
117
|
+
deleteOriginalAssets: Boolean(deleteOriginalAssets),
|
|
118
|
+
skipIfLargerOrEqual: Boolean(skipIfLargerOrEqual),
|
|
119
|
+
concurrency,
|
|
120
|
+
logLevel,
|
|
121
|
+
enableInWatchMode: Boolean(enableInWatchMode)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the emitted file name for a compressed artifact.
|
|
126
|
+
*
|
|
127
|
+
* Supported tokens: `[path]`, `[base]`, `[name]`, `[ext]`, `[hash]`.
|
|
128
|
+
*/
|
|
129
|
+
function resolveOutputFileName(filename, fileName, algorithm, source) {
|
|
130
|
+
if (typeof filename === "function") return filename(fileName, algorithm.algorithm);
|
|
131
|
+
const pattern = filename ?? `[path][base]${algorithm.extension}`;
|
|
132
|
+
const dir = path.posix.dirname(fileName);
|
|
133
|
+
const base = path.posix.basename(fileName);
|
|
134
|
+
const ext = path.posix.extname(fileName);
|
|
135
|
+
return pattern.replaceAll("[path]", dir === "." ? "" : `${dir}/`).replaceAll("[base]", base).replaceAll("[name]", base.slice(0, base.length - ext.length)).replaceAll("[ext]", ext).replaceAll("[hash]", () => createHash("sha256").update(source).digest("hex").slice(0, 8));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Compress a batch of buffers in the native rayon thread pool, off the JS
|
|
139
|
+
* main thread. `tasks[i]` describes how to compress `buffers[i]`.
|
|
140
|
+
*/
|
|
141
|
+
const compressBuffers = createRequire(import.meta.url)("../index.js").compressBuffers;
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region ts/plugin.ts
|
|
144
|
+
const PLUGIN_NAME = "rolldown-compression";
|
|
145
|
+
const LOG_PRIORITY = {
|
|
146
|
+
silent: 0,
|
|
147
|
+
error: 1,
|
|
148
|
+
warn: 2,
|
|
149
|
+
info: 3
|
|
150
|
+
};
|
|
151
|
+
function createLogger(level) {
|
|
152
|
+
const priority = LOG_PRIORITY[level];
|
|
153
|
+
const prefix = `[${PLUGIN_NAME}]`;
|
|
154
|
+
return {
|
|
155
|
+
info: (message) => {
|
|
156
|
+
if (priority >= LOG_PRIORITY.info) console.info(`${prefix} ${message}`);
|
|
157
|
+
},
|
|
158
|
+
warn: (message) => {
|
|
159
|
+
if (priority >= LOG_PRIORITY.warn) console.warn(`${prefix} ${message}`);
|
|
160
|
+
},
|
|
161
|
+
error: (message) => {
|
|
162
|
+
if (priority >= LOG_PRIORITY.error) console.error(`${prefix} ${message}`);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function toBuffer(source) {
|
|
167
|
+
return typeof source === "string" ? Buffer.from(source, "utf8") : Buffer.from(source);
|
|
168
|
+
}
|
|
169
|
+
function formatBytes(bytes) {
|
|
170
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
171
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
172
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
173
|
+
}
|
|
174
|
+
function createCompressionPlugin(options) {
|
|
175
|
+
const logger = createLogger(options.logLevel);
|
|
176
|
+
const emittedNames = /* @__PURE__ */ new Set();
|
|
177
|
+
return Object.assign({
|
|
178
|
+
name: PLUGIN_NAME,
|
|
179
|
+
async generateBundle(_outputOptions, bundle) {
|
|
180
|
+
if (this.meta?.watchMode && !options.enableInWatchMode) {
|
|
181
|
+
logger.info("watch mode detected, skipping compression (set enableInWatchMode to opt in)");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const startedAt = performance.now();
|
|
185
|
+
const pending = [];
|
|
186
|
+
const processedSources = /* @__PURE__ */ new Set();
|
|
187
|
+
for (const [fileName, output] of Object.entries(bundle)) {
|
|
188
|
+
if (!options.filter(fileName)) continue;
|
|
189
|
+
if (COMPRESSED_EXTENSION_RE.test(fileName) || emittedNames.has(fileName)) continue;
|
|
190
|
+
const buffer = toBuffer(output.type === "chunk" ? output.code : output.source);
|
|
191
|
+
if (buffer.byteLength < options.threshold) continue;
|
|
192
|
+
for (const algorithm of options.algorithms) {
|
|
193
|
+
const outputFileName = resolveOutputFileName(options.filename, fileName, algorithm, buffer);
|
|
194
|
+
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`));
|
|
195
|
+
pending.push({
|
|
196
|
+
task: {
|
|
197
|
+
fileName,
|
|
198
|
+
algorithm: algorithm.algorithm,
|
|
199
|
+
level: algorithm.level,
|
|
200
|
+
windowBits: algorithm.windowBits
|
|
201
|
+
},
|
|
202
|
+
buffer,
|
|
203
|
+
sourceFileName: fileName,
|
|
204
|
+
outputFileName
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
processedSources.add(fileName);
|
|
208
|
+
}
|
|
209
|
+
if (pending.length === 0) return;
|
|
210
|
+
const results = await compressBuffers(pending.map((artifact) => artifact.task), pending.map((artifact) => artifact.buffer), {
|
|
211
|
+
concurrency: options.concurrency,
|
|
212
|
+
skipIfLargerOrEqual: options.skipIfLargerOrEqual
|
|
213
|
+
});
|
|
214
|
+
const failures = [];
|
|
215
|
+
const emittedBySource = /* @__PURE__ */ new Map();
|
|
216
|
+
const stats = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const [index, result] of results.entries()) {
|
|
218
|
+
const artifact = pending[index];
|
|
219
|
+
if (artifact === void 0) continue;
|
|
220
|
+
if (result.error !== void 0 && result.error !== null) {
|
|
221
|
+
failures.push(`${result.fileName} (${result.algorithm}): ${result.error}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (result.skipped) {
|
|
225
|
+
logger.info(`skipped ${artifact.outputFileName}: ${result.algorithm} output would not be smaller than the original`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
this.emitFile({
|
|
229
|
+
type: "asset",
|
|
230
|
+
fileName: artifact.outputFileName,
|
|
231
|
+
source: result.data
|
|
232
|
+
});
|
|
233
|
+
emittedNames.add(artifact.outputFileName);
|
|
234
|
+
emittedBySource.set(artifact.sourceFileName, (emittedBySource.get(artifact.sourceFileName) ?? 0) + 1);
|
|
235
|
+
const stat = stats.get(result.algorithm) ?? {
|
|
236
|
+
count: 0,
|
|
237
|
+
originalBytes: 0,
|
|
238
|
+
outputBytes: 0
|
|
239
|
+
};
|
|
240
|
+
stat.count += 1;
|
|
241
|
+
stat.originalBytes += result.originalSize;
|
|
242
|
+
stat.outputBytes += result.compressedSize;
|
|
243
|
+
stats.set(result.algorithm, stat);
|
|
244
|
+
}
|
|
245
|
+
if (failures.length > 0) this.error(/* @__PURE__ */ new Error(`[${PLUGIN_NAME}] ${failures.length} compression task(s) failed:\n${failures.join("\n")}`));
|
|
246
|
+
if (options.deleteOriginalAssets) for (const fileName of processedSources) {
|
|
247
|
+
if ((emittedBySource.get(fileName) ?? 0) === 0) logger.warn(`deleteOriginalAssets removed "${fileName}" even though no compressed variant was emitted for it`);
|
|
248
|
+
delete bundle[fileName];
|
|
249
|
+
}
|
|
250
|
+
const elapsedMs = performance.now() - startedAt;
|
|
251
|
+
const summary = [...stats.entries()].map(([algorithm, stat]) => `${algorithm}: ${stat.count} file(s), ${formatBytes(stat.originalBytes)} -> ${formatBytes(stat.outputBytes)}`).join("; ");
|
|
252
|
+
if (summary.length > 0) {
|
|
253
|
+
const totalSaved = [...stats.values()].reduce((sum, stat) => sum + (stat.originalBytes - stat.outputBytes), 0);
|
|
254
|
+
logger.info(`${summary}; saved ${formatBytes(totalSaved)} in ${(elapsedMs / 1e3).toFixed(2)}s`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}, { apply: "build" });
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region ts/index.ts
|
|
261
|
+
/**
|
|
262
|
+
* Create the compression plugin.
|
|
263
|
+
*
|
|
264
|
+
* Options are validated eagerly: invalid levels, algorithm names, thresholds
|
|
265
|
+
* etc. throw here rather than during the build.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* import { defineConfig } from 'rolldown'
|
|
269
|
+
* import { compression, defineAlgorithm } from '@medicomind/rolldown-compression'
|
|
270
|
+
*
|
|
271
|
+
* export default defineConfig({
|
|
272
|
+
* plugins: [
|
|
273
|
+
* compression({
|
|
274
|
+
* threshold: 1024,
|
|
275
|
+
* algorithms: ['gzip', defineAlgorithm('brotli', { quality: 11 })],
|
|
276
|
+
* }),
|
|
277
|
+
* ],
|
|
278
|
+
* })
|
|
279
|
+
*/
|
|
280
|
+
function compression(options) {
|
|
281
|
+
return createCompressionPlugin(resolveOptions(options));
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
export { compression, defineAlgorithm };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* auto-generated by NAPI-RS */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
/** Batch-wide options for [`compress_buffers`]. */
|
|
4
|
+
export interface BatchOptions {
|
|
5
|
+
/** Worker threads; 0 or omitted = number of logical CPUs. */
|
|
6
|
+
concurrency?: number
|
|
7
|
+
/**
|
|
8
|
+
* Mark results whose compressed size >= original as skipped and
|
|
9
|
+
* return no data for them. Default: false.
|
|
10
|
+
*/
|
|
11
|
+
skipIfLargerOrEqual?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compress a batch of buffers off the JS main thread.
|
|
16
|
+
*
|
|
17
|
+
* `tasks[i]` describes how to compress `buffers[i]`. Algorithm names and
|
|
18
|
+
* levels are validated synchronously so misconfiguration fails fast;
|
|
19
|
+
* I/O-shaped failures during compression are reported per task via
|
|
20
|
+
* [`CompressResult::error`].
|
|
21
|
+
*/
|
|
22
|
+
export declare function compressBuffers(tasks: Array<CompressTask>, buffers: Array<Buffer>, options?: BatchOptions | undefined | null): Promise<Array<CompressResult>>
|
|
23
|
+
|
|
24
|
+
/** Outcome of a single task within a batch. */
|
|
25
|
+
export interface CompressResult {
|
|
26
|
+
fileName: string
|
|
27
|
+
algorithm: string
|
|
28
|
+
/** Compressed bytes. Empty when `skipped` is true or `error` is set. */
|
|
29
|
+
data: Buffer
|
|
30
|
+
originalSize: number
|
|
31
|
+
compressedSize: number
|
|
32
|
+
/**
|
|
33
|
+
* True when compression would not shrink the input and
|
|
34
|
+
* `skipIfLargerOrEqual` was requested.
|
|
35
|
+
*/
|
|
36
|
+
skipped: boolean
|
|
37
|
+
/** Per-task failure. A failed task never aborts the rest of the batch. */
|
|
38
|
+
error?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One compression task: pairs with the buffer at the same index in the
|
|
43
|
+
* `buffers` argument of [`compress_buffers`].
|
|
44
|
+
*/
|
|
45
|
+
export interface CompressTask {
|
|
46
|
+
fileName: string
|
|
47
|
+
/** Canonical algorithm name: "gzip" | "brotli" | "zstd". */
|
|
48
|
+
algorithm: string
|
|
49
|
+
/**
|
|
50
|
+
* Compression level; algorithm default when omitted
|
|
51
|
+
* (gzip 6, brotli 11, zstd 19).
|
|
52
|
+
*/
|
|
53
|
+
level?: number
|
|
54
|
+
/** Brotli only: log2 window size (10-24, default 22). */
|
|
55
|
+
windowBits?: number
|
|
56
|
+
}
|