zcompress-vite-plugin 0.1.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/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # zcompress Vite Plugin
2
+
3
+ High-performance multi-threaded asset compression for Vite production builds, powered by [zcompress](https://github.com/luochuan2008/zcompress) (Zig).
4
+
5
+ ## Features
6
+
7
+ - ๐Ÿงต **Multi-threaded** โ€” all CPU cores, not just one
8
+ - โšก **5-10x faster** than Node.js compression plugins
9
+ - ๐Ÿ“ฆ **gzip**, **zstd**, **brotli** โ€” all three algorithms
10
+ - ๐Ÿ’พ **Incremental cache** โ€” skip unchanged files
11
+
12
+ ## Quick Start
13
+
14
+ ```bash
15
+ npm install zcompress-vite-plugin --save-dev
16
+ ```
17
+
18
+ ```js
19
+ // vite.config.js
20
+ import zcompress from 'zcompress-vite-plugin';
21
+
22
+ export default {
23
+ plugins: [
24
+ zcompress({
25
+ algo: 'gzip', // 'gzip' | 'zstd' | 'brotli'
26
+ level: 6, // 1-9
27
+ threads: 4, // 0 = auto
28
+ cache: true, // skip unchanged files
29
+ })
30
+ ]
31
+ };
32
+ ```
33
+
34
+ After `vite build`, you'll get a `dist-compressed/` folder with `.gz` (or `.zst`/`.br`) files.
35
+
36
+ ## How It Works
37
+
38
+ The plugin ships a JS wrapper. On `npm install`, it downloads the prebuilt Zig binary for your platform from GitHub Releases. If the download fails (air-gapped, unsupported arch), it falls back to looking for `zcompress` in your PATH.
39
+
40
+ ```bash
41
+ # Manual install (if auto-download fails)
42
+ brew install zcompress # macOS
43
+ zig build install # from source
44
+ ```
45
+
46
+ ## Options
47
+
48
+ | Option | Type | Default | Description |
49
+ |--------|------|---------|-------------|
50
+ | `algo` | `'gzip' \| 'zstd' \| 'brotli'` | `'gzip'` | Compression algorithm |
51
+ | `level` | `number` | `6` | Compression level (1-9) |
52
+ | `threads` | `number` | `0` | Thread count (0 = auto) |
53
+ | `verbose` | `boolean` | `false` | Verbose output |
54
+ | `cache` | `boolean` | `false` | Skip unchanged files |
55
+ | `include` | `string[]` | `[]` | Extra extensions to compress |
56
+ | `exclude` | `string[]` | `[]` | Extensions to skip |
57
+ | `binaryPath` | `string` | auto | Override binary path |
58
+
59
+ ## License
60
+
61
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ declare module 'zcompress-vite-plugin' {
2
+ interface ZCompressOptions {
3
+ /** Compression algorithm: 'gzip' | 'zstd' | 'brotli' (default: 'gzip') */
4
+ algo?: 'gzip' | 'zstd' | 'brotli';
5
+ /** Compression level: 1-9 (default: 6) */
6
+ level?: number;
7
+ /** Thread count, 0 = auto (default: 0) */
8
+ threads?: number;
9
+ /** Verbose output (default: false) */
10
+ verbose?: boolean;
11
+ /** Enable incremental cache (default: false) */
12
+ cache?: boolean;
13
+ /** Extra file extensions to include (e.g. ['.ts', '.tsx']) */
14
+ include?: string[];
15
+ /** File extensions to exclude (e.g. ['.map']) */
16
+ exclude?: string[];
17
+ /** Override path to zcompress binary */
18
+ binaryPath?: string;
19
+ }
20
+
21
+ import type { Plugin } from 'vite';
22
+
23
+ function zcompressPlugin(options?: ZCompressOptions): Plugin;
24
+ export default zcompressPlugin;
25
+ }
package/index.js ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * zcompress Vite Plugin
3
+ *
4
+ * A Vite plugin that compresses build output using the zcompress CLI.
5
+ * Provides multi-threaded gzip/zstd compression for production builds.
6
+ *
7
+ * @example
8
+ * // vite.config.js
9
+ * import zcompress from 'zcompress-vite-plugin';
10
+ *
11
+ * export default {
12
+ * plugins: [
13
+ * zcompress({ algo: 'gzip', level: 6, threads: 4 })
14
+ * ]
15
+ * };
16
+ */
17
+
18
+ import { execSync } from 'node:child_process';
19
+ import { existsSync, statSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+
22
+ /**
23
+ * @typedef {Object} ZCompressOptions
24
+ * @property {string} [algo='gzip'] - Compression algorithm: gzip, zstd, brotli
25
+ * @property {number} [level=6] - Compression level: 1-9
26
+ * @property {number} [threads=0] - Thread count (0 = auto)
27
+ * @property {boolean} [verbose=false] - Verbose output
28
+ * @property {boolean} [cache=false] - Enable incremental caching
29
+ * @property {string[]} [include] - Extra file extensions to include
30
+ * @property {string[]} [exclude] - File extensions to exclude
31
+ * @property {string} [binaryPath] - Path to zcompress binary
32
+ */
33
+
34
+ /** @type {ZCompressOptions} */
35
+ const DEFAULT_OPTIONS = {
36
+ algo: 'gzip',
37
+ level: 6,
38
+ threads: 0,
39
+ verbose: false,
40
+ cache: false,
41
+ include: [],
42
+ exclude: [],
43
+ };
44
+
45
+ /**
46
+ * Find the zcompress binary.
47
+ * Order: 1) downloaded by postinstall 2) PATH 3) local zig build
48
+ */
49
+ function findBinary() {
50
+ // 1. Check for postinstall-downloaded binary
51
+ const binDir = join(__dirname, 'bin');
52
+ const localBin = join(binDir, process.platform === 'win32' ? 'zcompress.exe' : 'zcompress');
53
+ if (existsSync(localBin)) return localBin;
54
+
55
+ // 2. Check local zig build
56
+ const zigPaths = [
57
+ join(process.cwd(), 'zig-out', 'bin', 'zcompress'),
58
+ join(process.cwd(), 'zig-out', 'bin', 'zcompress.exe'),
59
+ ];
60
+ for (const p of zigPaths) {
61
+ if (existsSync(p)) return p;
62
+ }
63
+
64
+ // 3. Fall back to PATH
65
+ return 'zcompress';
66
+ }
67
+
68
+ /**
69
+ * Create the zcompress Vite plugin.
70
+ *
71
+ * @param {ZCompressOptions} userOptions
72
+ * @returns {import('vite').Plugin}
73
+ */
74
+ export default function zcompressPlugin(userOptions = {}) {
75
+ const options = { ...DEFAULT_OPTIONS, ...userOptions };
76
+
77
+ return {
78
+ name: 'zcompress',
79
+ apply: 'build',
80
+ enforce: 'post',
81
+
82
+ configResolved(config) {
83
+ // Only run in production build mode
84
+ this.active = config.command === 'build';
85
+ },
86
+
87
+ closeBundle() {
88
+ if (!this.active) return;
89
+
90
+ const outDir = this.outDir || 'dist';
91
+ const compressedDir = `${outDir}-compressed`;
92
+
93
+ if (!existsSync(outDir)) {
94
+ console.warn(`[zcompress] Output directory "${outDir}" not found. Skipping.`);
95
+ return;
96
+ }
97
+
98
+ const binary = options.binaryPath || findBinary();
99
+ const args = [
100
+ '-i', outDir,
101
+ '-o', compressedDir,
102
+ '-a', options.algo,
103
+ '-l', String(options.level),
104
+ ];
105
+
106
+ if (options.threads > 0) {
107
+ args.push('-t', String(options.threads));
108
+ }
109
+ if (options.verbose) args.push('--verbose');
110
+ if (options.cache) args.push('--cache');
111
+
112
+ for (const ext of options.include) {
113
+ args.push(`--include=${ext}`);
114
+ }
115
+ for (const ext of options.exclude) {
116
+ args.push(`--exclude=${ext}`);
117
+ }
118
+
119
+ const cmd = `${binary} ${args.join(' ')}`;
120
+
121
+ if (options.verbose) {
122
+ console.log(`[zcompress] Running: ${cmd}`);
123
+ }
124
+
125
+ const startTime = Date.now();
126
+
127
+ try {
128
+ execSync(cmd, { stdio: 'inherit' });
129
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
130
+
131
+ // Calculate size stats
132
+ const srcSize = getDirSize(outDir);
133
+ const destSize = getDirSize(compressedDir);
134
+ const savedPct = srcSize > 0
135
+ ? ((1 - destSize / srcSize) * 100).toFixed(1)
136
+ : '0.0';
137
+
138
+ console.log(`[zcompress] โœ… Compressed ${formatSize(srcSize)} โ†’ ${formatSize(destSize)} (saved ${savedPct}%) in ${elapsed}s`);
139
+ } catch (err) {
140
+ console.error(`[zcompress] โŒ Compression failed: ${err.message}`);
141
+ console.error('[zcompress] Make sure zcompress is installed: zig build install');
142
+ }
143
+ },
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Calculate total directory size in bytes.
149
+ *
150
+ * @param {string} dirPath
151
+ * @returns {number}
152
+ */
153
+ function getDirSize(dirPath) {
154
+ if (!existsSync(dirPath)) return 0;
155
+ // Simple approximation โ€” list files and sum their sizes
156
+ let total = 0;
157
+ try {
158
+ const { readdirSync } = require('node:fs');
159
+ const { join } = require('node:path');
160
+ walkDir(dirPath, (filePath) => {
161
+ try {
162
+ total += statSync(filePath).size;
163
+ } catch (_) { /* ignore */ }
164
+ });
165
+ } catch (_) { /* ignore */ }
166
+ return total;
167
+ }
168
+
169
+ /**
170
+ * Recursively walk a directory.
171
+ *
172
+ * @param {string} dir
173
+ * @param {(path: string) => void} fn
174
+ */
175
+ function walkDir(dir, fn) {
176
+ const { readdirSync } = require('node:fs');
177
+ const { join } = require('node:path');
178
+ try {
179
+ const entries = readdirSync(dir, { withFileTypes: true });
180
+ for (const entry of entries) {
181
+ const full = join(dir, entry.name);
182
+ if (entry.isDirectory()) {
183
+ walkDir(full, fn);
184
+ } else {
185
+ fn(full);
186
+ }
187
+ }
188
+ } catch (_) { /* ignore */ }
189
+ }
190
+
191
+ /**
192
+ * Format bytes into human-readable string.
193
+ *
194
+ * @param {number} bytes
195
+ * @returns {string}
196
+ */
197
+ function formatSize(bytes) {
198
+ if (bytes === 0) return '0 B';
199
+ const units = ['B', 'KB', 'MB', 'GB'];
200
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
201
+ const size = (bytes / Math.pow(1024, i)).toFixed(1);
202
+ return `${size} ${units[Math.min(i, units.length - 1)]}`;
203
+ }
package/install.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script โ€” downloads the prebuilt zcompress binary from GitHub Releases.
5
+ * Falls back gracefully if the binary can't be downloaded (e.g. air-gapped, unsupported platform).
6
+ */
7
+
8
+ import { execSync } from 'node:child_process';
9
+ import { createWriteStream, existsSync, chmodSync, mkdirSync, unlinkSync } from 'node:fs';
10
+ import { get } from 'node:https';
11
+ import { join, dirname } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { pipeline } from 'node:stream/promises';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const BIN_DIR = join(__dirname, 'bin');
17
+ const VERSION = '0.1.0'; // Update on each release
18
+ const REPO = 'luochuan2008/zcompress'; // Change to your GitHub username/repo
19
+
20
+ const PLATFORM_MAP = {
21
+ 'darwin-arm64': 'zcompress-macos-arm64',
22
+ 'darwin-x64': 'zcompress-macos-x64',
23
+ 'linux-x64': 'zcompress-linux-x64',
24
+ 'win32-x64': 'zcompress-windows-x64.exe',
25
+ };
26
+
27
+ function getPlatformKey() {
28
+ const os = process.platform;
29
+ const arch = process.arch === 'x64' ? 'x64' : process.arch === 'arm64' ? 'arm64' : process.arch;
30
+ return `${os}-${arch}`;
31
+ }
32
+
33
+ function getBinaryName() {
34
+ const key = getPlatformKey();
35
+ return PLATFORM_MAP[key] || null;
36
+ }
37
+
38
+ async function downloadBinary() {
39
+ const binaryName = getBinaryName();
40
+ if (!binaryName) {
41
+ console.log(`[zcompress] โš  Unsupported platform: ${getPlatformKey()}. Build from source: zig build install`);
42
+ return false;
43
+ }
44
+
45
+ const destPath = join(BIN_DIR, process.platform === 'win32' ? 'zcompress.exe' : 'zcompress');
46
+
47
+ // Skip if already downloaded
48
+ if (existsSync(destPath)) {
49
+ console.log(`[zcompress] โœ“ Binary already installed: ${destPath}`);
50
+ return true;
51
+ }
52
+
53
+ const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${binaryName}`;
54
+ console.log(`[zcompress] โ†“ Downloading ${url} ...`);
55
+
56
+ try {
57
+ mkdirSync(BIN_DIR, { recursive: true });
58
+
59
+ await new Promise((resolve, reject) => {
60
+ const file = createWriteStream(destPath);
61
+ get(url, (response) => {
62
+ // Follow redirects
63
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
64
+ get(response.headers.location, (redirectRes) => {
65
+ pipeline(redirectRes, file).then(resolve).catch(reject);
66
+ }).on('error', reject);
67
+ return;
68
+ }
69
+ if (response.statusCode !== 200) {
70
+ file.close();
71
+ unlinkSync(destPath);
72
+ reject(new Error(`HTTP ${response.statusCode}`));
73
+ return;
74
+ }
75
+ pipeline(response, file).then(resolve).catch(reject);
76
+ }).on('error', reject);
77
+ });
78
+
79
+ // Make executable on Unix
80
+ if (process.platform !== 'win32') {
81
+ chmodSync(destPath, 0o755);
82
+ }
83
+
84
+ console.log(`[zcompress] โœ“ Binary installed: ${destPath}`);
85
+ return true;
86
+ } catch (err) {
87
+ console.log(`[zcompress] โš  Could not download binary (${err.message}). Build from source: cd zcompress && zig build install`);
88
+ // Clean up partial download
89
+ try { unlinkSync(destPath); } catch (_) { /* ignore */ }
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // Run download, but don't fail install if it doesn't work
95
+ downloadBinary().catch(() => {
96
+ console.log('[zcompress] โ„น Binary download skipped. The plugin requires the zcompress CLI.');
97
+ console.log('[zcompress] โ„น Install it via: brew install zcompress OR zig build install');
98
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "zcompress-vite-plugin",
3
+ "version": "0.1.0",
4
+ "description": "High-performance multi-threaded asset compression for Vite โ€” powered by Zig",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "files": [
9
+ "index.js",
10
+ "index.d.ts",
11
+ "install.js",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "postinstall": "node install.js",
16
+ "test": "node test.js"
17
+ },
18
+ "keywords": [
19
+ "vite",
20
+ "vite-plugin",
21
+ "compression",
22
+ "gzip",
23
+ "zstd",
24
+ "brotli",
25
+ "assets",
26
+ "zig",
27
+ "performance",
28
+ "multi-threaded"
29
+ ],
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/luochuan2008/zcompress"
34
+ },
35
+ "peerDependencies": {
36
+ "vite": "^4.0.0 || ^5.0.0 || ^6.0.0"
37
+ },
38
+ "engines": {
39
+ "node": ">=16.0.0"
40
+ }
41
+ }