zcompress-vite-plugin 0.1.4 → 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.
- package/README.md +35 -0
- package/index.js +118 -6
- package/install.js +90 -45
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -74,8 +74,17 @@ zcompress({
|
|
|
74
74
|
})
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
也可用环境变量(不改代码):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export ZCOMPRESS_BINARY=/absolute/path/to/zcompress
|
|
81
|
+
npm run build
|
|
82
|
+
```
|
|
83
|
+
|
|
77
84
|
默认 `failOnError: true`,失败会直接中断构建(不会静默跳过压缩)。
|
|
78
85
|
|
|
86
|
+
从 `v0.1.6` 开始:若找不到二进制,会自动启用 Node 内置 fallback(支持 `gzip` / `brotli`)。`zstd` 仍需 CLI 二进制。
|
|
87
|
+
|
|
79
88
|
## License
|
|
80
89
|
|
|
81
90
|
MIT
|
|
@@ -161,8 +170,34 @@ zcompress({
|
|
|
161
170
|
})
|
|
162
171
|
```
|
|
163
172
|
|
|
173
|
+
Or use env var (no code change):
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
export ZCOMPRESS_BINARY=/absolute/path/to/zcompress
|
|
177
|
+
npm run build
|
|
178
|
+
```
|
|
179
|
+
|
|
164
180
|
Default `failOnError: true` ensures builds fail loudly (instead of silently skipping compression).
|
|
165
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
|
+
|
|
166
201
|
## License
|
|
167
202
|
|
|
168
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
|
|
@@ -49,14 +55,18 @@ const DEFAULT_OPTIONS = {
|
|
|
49
55
|
|
|
50
56
|
/**
|
|
51
57
|
* Find the zcompress binary.
|
|
52
|
-
* Order: 1) downloaded by postinstall
|
|
58
|
+
* Order: 1) ZCOMPRESS_BINARY env 2) downloaded by postinstall 3) local zig build 4) PATH
|
|
53
59
|
*/
|
|
54
60
|
function findBinary() {
|
|
55
|
-
// 1)
|
|
61
|
+
// 1) explicit env override
|
|
62
|
+
const envBinary = process.env.ZCOMPRESS_BINARY;
|
|
63
|
+
if (envBinary) return envBinary;
|
|
64
|
+
|
|
65
|
+
// 2) postinstall-downloaded binary inside package
|
|
56
66
|
const packagedBin = join(__dirname, 'bin', process.platform === 'win32' ? 'zcompress.exe' : 'zcompress');
|
|
57
67
|
if (existsSync(packagedBin)) return packagedBin;
|
|
58
68
|
|
|
59
|
-
//
|
|
69
|
+
// 3) local zig build
|
|
60
70
|
const zigPaths = [
|
|
61
71
|
join(process.cwd(), 'zig-out', 'bin', 'zcompress'),
|
|
62
72
|
join(process.cwd(), 'zig-out', 'bin', 'zcompress.exe'),
|
|
@@ -65,7 +75,7 @@ function findBinary() {
|
|
|
65
75
|
if (existsSync(p)) return p;
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
//
|
|
78
|
+
// 4) PATH
|
|
69
79
|
return 'zcompress';
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -143,6 +153,33 @@ export default function zcompressPlugin(userOptions = {}) {
|
|
|
143
153
|
|
|
144
154
|
console.log(`[zcompress] ✅ Compressed ${formatSize(srcSize)} → ${formatSize(destSize)} (saved ${savedPct}%) in ${elapsed}s`);
|
|
145
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
|
+
|
|
146
183
|
const message = [
|
|
147
184
|
`[zcompress] ❌ Compression failed: ${err.message}`,
|
|
148
185
|
`[zcompress] Binary used: ${binary}`,
|
|
@@ -160,6 +197,81 @@ export default function zcompressPlugin(userOptions = {}) {
|
|
|
160
197
|
};
|
|
161
198
|
}
|
|
162
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
|
+
|
|
163
275
|
/**
|
|
164
276
|
* Calculate total directory size in bytes.
|
|
165
277
|
*
|
package/install.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Postinstall script — downloads the prebuilt zcompress binary from GitHub Releases.
|
|
5
|
-
* Falls back gracefully if
|
|
5
|
+
* Falls back gracefully if binary can't be fetched.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { createWriteStream, existsSync, chmodSync, mkdirSync, unlinkSync, readFileSync } from 'node:fs';
|
|
@@ -20,10 +20,10 @@ const pkgJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')
|
|
|
20
20
|
const VERSION = pkgJson.version;
|
|
21
21
|
|
|
22
22
|
const PLATFORM_MAP = {
|
|
23
|
-
'darwin-arm64':
|
|
24
|
-
'darwin-x64':
|
|
25
|
-
'linux-x64':
|
|
26
|
-
'win32-x64':
|
|
23
|
+
'darwin-arm64': 'zcompress-macos-arm64',
|
|
24
|
+
'darwin-x64': 'zcompress-macos-x64',
|
|
25
|
+
'linux-x64': 'zcompress-linux-x64',
|
|
26
|
+
'win32-x64': 'zcompress-windows-x64.exe',
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
function getPlatformKey() {
|
|
@@ -33,8 +33,54 @@ function getPlatformKey() {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function getBinaryName() {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
return PLATFORM_MAP[getPlatformKey()] || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function request(url) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
get(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
'User-Agent': 'zcompress-vite-plugin-install',
|
|
44
|
+
Accept: 'application/vnd.github+json',
|
|
45
|
+
},
|
|
46
|
+
}, (res) => resolve(res)).on('error', reject);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchJson(url) {
|
|
51
|
+
const res = await request(url);
|
|
52
|
+
if (res.statusCode !== 200) {
|
|
53
|
+
throw new Error(`HTTP ${res.statusCode}`);
|
|
54
|
+
}
|
|
55
|
+
const chunks = [];
|
|
56
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
57
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function downloadToFile(url, destPath) {
|
|
61
|
+
const res = await request(url);
|
|
62
|
+
|
|
63
|
+
// Follow redirect (GitHub release assets usually 302)
|
|
64
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
65
|
+
return downloadToFile(res.headers.location, destPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (res.statusCode !== 200) {
|
|
69
|
+
throw new Error(`HTTP ${res.statusCode}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const file = createWriteStream(destPath);
|
|
73
|
+
await pipeline(res, file);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function resolveFallbackAssetUrl(binaryName) {
|
|
77
|
+
// Fallback strategy: use latest release asset for this platform when current version asset is missing.
|
|
78
|
+
const latest = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
79
|
+
const asset = (latest.assets || []).find((a) => a.name === binaryName);
|
|
80
|
+
if (!asset?.browser_download_url) {
|
|
81
|
+
throw new Error('Latest release has no matching asset');
|
|
82
|
+
}
|
|
83
|
+
return asset.browser_download_url;
|
|
38
84
|
}
|
|
39
85
|
|
|
40
86
|
async function downloadBinary() {
|
|
@@ -52,56 +98,55 @@ async function downloadBinary() {
|
|
|
52
98
|
return true;
|
|
53
99
|
}
|
|
54
100
|
|
|
55
|
-
|
|
56
|
-
console.log(`[zcompress] ↓ Downloading ${url} ...`);
|
|
101
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
57
102
|
|
|
103
|
+
const versionedUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/${binaryName}`;
|
|
104
|
+
const tried = [];
|
|
105
|
+
|
|
106
|
+
// Attempt 1: exact npm version
|
|
58
107
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await
|
|
62
|
-
const file = createWriteStream(destPath);
|
|
63
|
-
get(url, (response) => {
|
|
64
|
-
// Follow redirects
|
|
65
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
66
|
-
get(response.headers.location, (redirectRes) => {
|
|
67
|
-
pipeline(redirectRes, file).then(resolve).catch(reject);
|
|
68
|
-
}).on('error', reject);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (response.statusCode !== 200) {
|
|
72
|
-
file.close();
|
|
73
|
-
unlinkSync(destPath);
|
|
74
|
-
reject(new Error(`HTTP ${response.statusCode}`));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
pipeline(response, file).then(resolve).catch(reject);
|
|
78
|
-
}).on('error', reject);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Make executable on Unix
|
|
82
|
-
if (process.platform !== 'win32') {
|
|
83
|
-
chmodSync(destPath, 0o755);
|
|
84
|
-
}
|
|
108
|
+
tried.push(versionedUrl);
|
|
109
|
+
console.log(`[zcompress] ↓ Downloading ${versionedUrl} ...`);
|
|
110
|
+
await downloadToFile(versionedUrl, destPath);
|
|
85
111
|
|
|
112
|
+
if (process.platform !== 'win32') chmodSync(destPath, 0o755);
|
|
86
113
|
console.log(`[zcompress] ✓ Binary installed: ${destPath}`);
|
|
87
114
|
return true;
|
|
88
115
|
} catch (err) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
// cleanup partial
|
|
117
|
+
try { unlinkSync(destPath); } catch {}
|
|
118
|
+
|
|
119
|
+
// If 404 or similar, try latest release as fallback
|
|
120
|
+
const msg = String(err?.message || err);
|
|
121
|
+
if (!msg.includes('404')) {
|
|
122
|
+
console.log(`[zcompress] ⚠ Download failed (${msg}).`);
|
|
93
123
|
} else {
|
|
94
|
-
console.log(`[zcompress] ⚠
|
|
124
|
+
console.log(`[zcompress] ⚠ Asset missing for v${VERSION}: ${binaryName}`);
|
|
95
125
|
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Attempt 2: latest release fallback
|
|
129
|
+
try {
|
|
130
|
+
const latestAssetUrl = await resolveFallbackAssetUrl(binaryName);
|
|
131
|
+
tried.push(latestAssetUrl);
|
|
132
|
+
console.log(`[zcompress] ↓ Falling back to latest release asset: ${latestAssetUrl}`);
|
|
133
|
+
await downloadToFile(latestAssetUrl, destPath);
|
|
134
|
+
|
|
135
|
+
if (process.platform !== 'win32') chmodSync(destPath, 0o755);
|
|
136
|
+
console.log(`[zcompress] ✓ Binary installed from latest release: ${destPath}`);
|
|
137
|
+
return true;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
try { unlinkSync(destPath); } catch {}
|
|
140
|
+
console.log(`[zcompress] ⚠ Fallback download failed (${err.message}).`);
|
|
141
|
+
console.log('[zcompress] Tried URLs:');
|
|
142
|
+
for (const u of tried) console.log(` - ${u}`);
|
|
96
143
|
console.log('[zcompress] ℹ Build from source: cd zcompress && zig build -Doptimize=ReleaseFast');
|
|
97
|
-
// Clean up partial download
|
|
98
|
-
try { unlinkSync(destPath); } catch (_) { /* ignore */ }
|
|
99
144
|
return false;
|
|
100
145
|
}
|
|
101
146
|
}
|
|
102
147
|
|
|
103
148
|
// Run download, but don't fail install if it doesn't work
|
|
104
|
-
downloadBinary().catch(() => {
|
|
105
|
-
console.log(
|
|
106
|
-
console.log('[zcompress] ℹ Install it
|
|
149
|
+
downloadBinary().catch((err) => {
|
|
150
|
+
console.log(`[zcompress] ⚠ Binary download skipped (${err.message}).`);
|
|
151
|
+
console.log('[zcompress] ℹ Install it manually and set plugin option `binaryPath`.');
|
|
107
152
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zcompress-vite-plugin",
|
|
3
|
-
"version": "0.1.
|
|
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",
|