zcompress-vite-plugin 0.1.5 → 0.1.6

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.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/index.js +110 -2
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -83,6 +83,8 @@ npm run build
83
83
 
84
84
  默认 `failOnError: true`,失败会直接中断构建(不会静默跳过压缩)。
85
85
 
86
+ 从 `v0.1.6` 开始:若找不到二进制,会自动启用 Node 内置 fallback(支持 `gzip` / `brotli`)。`zstd` 仍需 CLI 二进制。
87
+
86
88
  ## License
87
89
 
88
90
  MIT
@@ -177,6 +179,25 @@ npm run build
177
179
 
178
180
  Default `failOnError: true` ensures builds fail loudly (instead of silently skipping compression).
179
181
 
182
+ Since `v0.1.6`: if the binary is missing, the plugin automatically falls back to Node built-in compression for `gzip` / `brotli`. `zstd` still requires the CLI binary.
183
+
184
+ ## Maintainer Release Checklist
185
+
186
+ Before running `npm publish`:
187
+
188
+ 1. Build and upload these assets to GitHub Release `vX.Y.Z`:
189
+ - `zcompress-macos-arm64`
190
+ - `zcompress-macos-x64`
191
+ - `zcompress-linux-x64`
192
+ - `zcompress-windows-x64.exe`
193
+ 2. Run:
194
+
195
+ ```bash
196
+ npm run verify:release-assets
197
+ ```
198
+
199
+ `prepublishOnly` enforces this automatically, so broken releases are blocked.
200
+
180
201
  ## License
181
202
 
182
203
  MIT
package/index.js CHANGED
@@ -16,12 +16,18 @@
16
16
  */
17
17
 
18
18
  import { execFileSync } from 'node:child_process';
19
- import { existsSync, statSync, readdirSync } from 'node:fs';
20
- import { join, dirname, isAbsolute } from 'node:path';
19
+ import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { join, dirname, isAbsolute, extname } from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
22
+ import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib';
22
23
 
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
25
 
26
+ const DEFAULT_EXTENSIONS = [
27
+ '.js', '.mjs', '.cjs', '.css', '.html', '.htm', '.json', '.svg',
28
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.ttf', '.woff', '.woff2', '.xml', '.csv', '.wasm',
29
+ ];
30
+
25
31
  /**
26
32
  * @typedef {Object} ZCompressOptions
27
33
  * @property {string} [algo='gzip'] - Compression algorithm: gzip, zstd, brotli
@@ -147,6 +153,33 @@ export default function zcompressPlugin(userOptions = {}) {
147
153
 
148
154
  console.log(`[zcompress] ✅ Compressed ${formatSize(srcSize)} → ${formatSize(destSize)} (saved ${savedPct}%) in ${elapsed}s`);
149
155
  } catch (err) {
156
+ // Binary missing: fallback to built-in Node compression for gzip/brotli.
157
+ if (isBinaryNotFoundError(err)) {
158
+ try {
159
+ const fb = fallbackCompressAssets(outDir, compressedDir, options);
160
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
161
+ const savedPct = fb.srcSize > 0
162
+ ? ((1 - fb.destSize / fb.srcSize) * 100).toFixed(1)
163
+ : '0.0';
164
+
165
+ console.warn('[zcompress] ⚠ zcompress binary not found, using built-in Node fallback compressor.');
166
+ console.log(`[zcompress] ✅ Compressed ${formatSize(fb.srcSize)} → ${formatSize(fb.destSize)} (saved ${savedPct}%) in ${elapsed}s`);
167
+ if (fb.skipped > 0) console.log(`[zcompress] 💾 ${fb.skipped} file(s) skipped (cache)`);
168
+ return;
169
+ } catch (fallbackErr) {
170
+ const fallbackMessage = [
171
+ `[zcompress] ❌ Compression failed: ${err.message}`,
172
+ `[zcompress] Binary used: ${binary}`,
173
+ `[zcompress] Fallback also failed: ${fallbackErr.message}`,
174
+ '[zcompress] Workaround: install CLI manually (`zig build -Doptimize=ReleaseFast`) and set `binaryPath`.',
175
+ ].join('\n');
176
+
177
+ if (options.failOnError !== false) throw new Error(fallbackMessage);
178
+ console.error(fallbackMessage);
179
+ return;
180
+ }
181
+ }
182
+
150
183
  const message = [
151
184
  `[zcompress] ❌ Compression failed: ${err.message}`,
152
185
  `[zcompress] Binary used: ${binary}`,
@@ -164,6 +197,81 @@ export default function zcompressPlugin(userOptions = {}) {
164
197
  };
165
198
  }
166
199
 
200
+ function isBinaryNotFoundError(err) {
201
+ return err && (err.code === 'ENOENT' || String(err.message || '').includes('ENOENT'));
202
+ }
203
+
204
+ function shouldCompressFile(filePath, options) {
205
+ const ext = extname(filePath).toLowerCase();
206
+ const include = options.include.length > 0
207
+ ? options.include.map((e) => e.toLowerCase())
208
+ : DEFAULT_EXTENSIONS;
209
+ const exclude = options.exclude.map((e) => e.toLowerCase());
210
+
211
+ if (!include.includes(ext)) return false;
212
+ if (exclude.includes(ext)) return false;
213
+ return true;
214
+ }
215
+
216
+ function compressionSuffix(algo) {
217
+ if (algo === 'gzip') return '.gz';
218
+ if (algo === 'brotli') return '.br';
219
+ throw new Error('Node fallback currently supports only gzip and brotli. Use CLI binary for zstd.');
220
+ }
221
+
222
+ function compressBuffer(buf, options) {
223
+ if (options.algo === 'gzip') {
224
+ const level = Math.max(1, Math.min(9, Number(options.level) || 6));
225
+ return gzipSync(buf, { level });
226
+ }
227
+ if (options.algo === 'brotli') {
228
+ const quality = Math.max(1, Math.min(11, Number(options.level) || 6));
229
+ return brotliCompressSync(buf, {
230
+ params: {
231
+ [zlibConstants.BROTLI_PARAM_QUALITY]: quality,
232
+ },
233
+ });
234
+ }
235
+ throw new Error('Node fallback currently supports only gzip and brotli.');
236
+ }
237
+
238
+ function fallbackCompressAssets(outDir, compressedDir, options) {
239
+ const allFiles = [];
240
+ walkDir(outDir, (file) => allFiles.push(file));
241
+
242
+ const suffix = compressionSuffix(options.algo);
243
+ let srcSize = 0;
244
+ let destSize = 0;
245
+ let skipped = 0;
246
+
247
+ for (const file of allFiles) {
248
+ if (!shouldCompressFile(file, options)) continue;
249
+
250
+ const rel = file.slice(outDir.length + 1);
251
+ const dest = join(compressedDir, `${rel}${suffix}`);
252
+
253
+ const srcStat = statSync(file);
254
+ if (options.cache && existsSync(dest)) {
255
+ const dstStat = statSync(dest);
256
+ if (dstStat.mtimeMs >= srcStat.mtimeMs) {
257
+ skipped++;
258
+ continue;
259
+ }
260
+ }
261
+
262
+ const data = readFileSync(file);
263
+ const compressed = compressBuffer(data, options);
264
+
265
+ mkdirSync(dirname(dest), { recursive: true });
266
+ writeFileSync(dest, compressed);
267
+
268
+ srcSize += srcStat.size;
269
+ destSize += compressed.length;
270
+ }
271
+
272
+ return { srcSize, destSize, skipped };
273
+ }
274
+
167
275
  /**
168
276
  * Calculate total directory size in bytes.
169
277
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zcompress-vite-plugin",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "High-performance multi-threaded asset compression for Vite — powered by Zig",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -13,7 +13,9 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "postinstall": "node install.js",
16
- "test": "node test.js"
16
+ "test": "node test.js",
17
+ "verify:release-assets": "node verify-release-assets.js",
18
+ "prepublishOnly": "node verify-release-assets.js --warn-only"
17
19
  },
18
20
  "keywords": [
19
21
  "vite",