@pz4l/tinyimg-unplugin 0.0.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,210 @@
1
+ [English](README.md) | [简体中文](README.zh-CN.md)
2
+
3
+ # tinyimg-unplugin
4
+
5
+ 基于 TinyPNG 的图片压缩 unplugin,支持 Vite、Webpack、Rolldown 等构建工具。
6
+
7
+ ## Supported Tools
8
+
9
+ - Vite
10
+ - Webpack
11
+ - Rolldown
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @pz4l/tinyimg-unplugin -D
17
+ ```
18
+
19
+ ## Environment Setup
20
+
21
+ ### TINYPNG_KEYS 环境变量
22
+
23
+ 在使用插件之前,你需要设置 `TINYPNG_KEYS` 环境变量,包含一个或多个 TinyPNG API key(用逗号分隔):
24
+
25
+ ```bash
26
+ # 单个 key
27
+ export TINYPNG_KEYS=your_api_key_here
28
+
29
+ # 多个 key(用逗号分隔)
30
+ export TINYPNG_KEYS=key1,key2,key3
31
+ ```
32
+
33
+ ### 如何获取 API Key
34
+
35
+ 1. 访问 [TinyPNG 开发者页面](https://tinypng.com/developers)
36
+ 2. 使用邮箱注册获取 API key
37
+
38
+ ### 在项目中配置环境变量
39
+
40
+ **Vite 项目**(`.env` 文件):
41
+
42
+ ```bash
43
+ TINYPNG_KEYS=your_api_key_here
44
+ ```
45
+
46
+ **Webpack 项目**(`webpack.config.js`):
47
+
48
+ ```javascript
49
+ const webpack = require('webpack')
50
+
51
+ module.exports = {
52
+ plugins: [
53
+ new webpack.DefinePlugin({
54
+ 'process.env.TINYPNG_KEYS': JSON.stringify('your_api_key_here')
55
+ })
56
+ ]
57
+ }
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### Vite
63
+
64
+ ```javascript
65
+ import tinyimg from '@pz4l/tinyimg-unplugin/vite'
66
+ // vite.config.js
67
+ import { defineConfig } from 'vite'
68
+
69
+ export default defineConfig({
70
+ plugins: [
71
+ tinyimg({
72
+ mode: 'random', // key 使用策略
73
+ cache: true, // 启用缓存
74
+ parallel: 8, // 并发数
75
+ verbose: false, // 详细日志
76
+ strict: false, // 严格模式
77
+ include: ['**/*.png', '**/*.jpg'], // 包含的文件
78
+ exclude: ['**/node_modules/**'] // 排除的文件
79
+ })
80
+ ]
81
+ })
82
+ ```
83
+
84
+ ### Webpack
85
+
86
+ ```javascript
87
+ // webpack.config.js
88
+ const tinyimg = require('@pz4l/tinyimg-unplugin/webpack')
89
+
90
+ module.exports = {
91
+ plugins: [
92
+ tinyimg.default({
93
+ mode: 'random',
94
+ cache: true,
95
+ parallel: 8,
96
+ verbose: false,
97
+ strict: false,
98
+ include: ['**/*.png', '**/*.jpg'],
99
+ exclude: ['**/node_modules/**']
100
+ })
101
+ ]
102
+ }
103
+ ```
104
+
105
+ ### Rolldown
106
+
107
+ ```javascript
108
+ // rolldown.config.js
109
+ import tinyimg from '@pz4l/tinyimg-unplugin/rolldown'
110
+
111
+ export default {
112
+ plugins: [
113
+ tinyimg({
114
+ mode: 'random',
115
+ cache: true,
116
+ parallel: 8,
117
+ verbose: false,
118
+ strict: false,
119
+ include: ['**/*.png', '**/*.jpg'],
120
+ exclude: ['**/node_modules/**']
121
+ })
122
+ ]
123
+ }
124
+ ```
125
+
126
+ ## Options
127
+
128
+ ### mode
129
+
130
+ - **Type**: `'random' | 'round-robin' | 'priority'`
131
+ - **Default**: `'random'`
132
+ - **Description**: API key 使用策略
133
+ - `random`: 随机选择 key(默认)
134
+ - `round-robin`: 轮询使用 key
135
+ - `priority`: 优先使用额度充足的 key
136
+
137
+ ### cache
138
+
139
+ - **Type**: `boolean`
140
+ - **Default**: `true`
141
+ - **Description**: 是否启用缓存。启用后,已压缩过的图片(基于 MD5)不会重复压缩,节省 API 额度。
142
+
143
+ ### parallel
144
+
145
+ - **Type**: `number`
146
+ - **Default**: `8`
147
+ - **Description**: 并发压缩数量。数值越高压缩越快,但会更快消耗 API 额度。
148
+
149
+ ### verbose
150
+
151
+ - **Type**: `boolean`
152
+ - **Default**: `false`
153
+ - **Description**: 是否输出详细日志。启用后会显示每张图片的压缩信息。
154
+
155
+ ### strict
156
+
157
+ - **Type**: `boolean`
158
+ - **Default**: `false`
159
+ - **Description**: 严格模式。启用后,如果压缩失败会中断构建;关闭则使用原图继续构建。
160
+
161
+ ### include
162
+
163
+ - **Type**: `string | string[]`
164
+ - **Default**: `undefined`
165
+ - **Description**: 包含的文件匹配模式。支持 glob 模式,如 `'**/*.png'`。
166
+
167
+ ### exclude
168
+
169
+ - **Type**: `string | string[]`
170
+ - **Default**: `undefined`
171
+ - **Description**: 排除的文件匹配模式。支持 glob 模式,如 `'**/node_modules/**'`。
172
+
173
+ ## Behavior
174
+
175
+ ### Production Only
176
+
177
+ 插件仅在生产构建时生效(`NODE_ENV=production` 或 Vite 的 build 模式),开发模式下不会压缩图片。
178
+
179
+ ### Supported Formats
180
+
181
+ 仅支持 TinyPNG 支持的格式:
182
+
183
+ - PNG
184
+ - JPG / JPEG
185
+
186
+ 其他格式(如 WebP、AVIF、SVG)会被忽略。
187
+
188
+ ### Project Cache Only
189
+
190
+ unplugin 仅使用项目级缓存(`.node_modules/.tinyimg_cache/`),不使用全局缓存。这确保每个项目的缓存相互隔离。
191
+
192
+ ### Key Management
193
+
194
+ - 仅从 `TINYPNG_KEYS` 环境变量读取 API key
195
+ - 不支持 CLI 的 `-k` 选项
196
+ - 自动轮询多个 key,当某个 key 额度耗尽时自动切换到下一个
197
+
198
+ ## Features
199
+
200
+ - Automatic image compression during build
201
+ - Multi-key management for quota optimization
202
+ - Smart caching based on MD5
203
+ - Concurrent compression control
204
+ - File filtering with glob patterns
205
+ - Production-only execution
206
+
207
+ ## Related Packages
208
+
209
+ - [@pz4l/tinyimg-core](https://github.com/pzehrel/tinyimg/tree/main/packages/tinyimg-core) - Core compression library
210
+ - [@pz4l/tinyimg-cli](https://github.com/pzehrel/tinyimg/tree/main/packages/tinyimg-cli) - CLI tool for image compression
@@ -0,0 +1,210 @@
1
+ [English](README.md) | 简体中文
2
+
3
+ # tinyimg-unplugin
4
+
5
+ 基于 TinyPNG 的图片压缩 unplugin,支持 Vite、Webpack、Rolldown 等构建工具。
6
+
7
+ ## 支持的工具
8
+
9
+ - Vite
10
+ - Webpack
11
+ - Rolldown
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ npm install @pz4l/tinyimg-unplugin -D
17
+ ```
18
+
19
+ ## 环境设置
20
+
21
+ ### TINYPNG_KEYS 环境变量
22
+
23
+ 在使用插件之前,你需要设置 `TINYPNG_KEYS` 环境变量,包含一个或多个 TinyPNG API key(用逗号分隔):
24
+
25
+ ```bash
26
+ # 单个 key
27
+ export TINYPNG_KEYS=your_api_key_here
28
+
29
+ # 多个 key(用逗号分隔)
30
+ export TINYPNG_KEYS=key1,key2,key3
31
+ ```
32
+
33
+ ### 如何获取 API Key
34
+
35
+ 1. 访问 [TinyPNG 开发者页面](https://tinypng.com/developers)
36
+ 2. 使用邮箱注册获取 API key
37
+
38
+ ### 在项目中配置环境变量
39
+
40
+ **Vite 项目**(`.env` 文件):
41
+
42
+ ```bash
43
+ TINYPNG_KEYS=your_api_key_here
44
+ ```
45
+
46
+ **Webpack 项目**(`webpack.config.js`):
47
+
48
+ ```javascript
49
+ const webpack = require('webpack')
50
+
51
+ module.exports = {
52
+ plugins: [
53
+ new webpack.DefinePlugin({
54
+ 'process.env.TINYPNG_KEYS': JSON.stringify('your_api_key_here')
55
+ })
56
+ ]
57
+ }
58
+ ```
59
+
60
+ ## 使用方法
61
+
62
+ ### Vite
63
+
64
+ ```javascript
65
+ import tinyimg from '@pz4l/tinyimg-unplugin/vite'
66
+ // vite.config.js
67
+ import { defineConfig } from 'vite'
68
+
69
+ export default defineConfig({
70
+ plugins: [
71
+ tinyimg({
72
+ mode: 'random', // key 使用策略
73
+ cache: true, // 启用缓存
74
+ parallel: 8, // 并发数
75
+ verbose: false, // 详细日志
76
+ strict: false, // 严格模式
77
+ include: ['**/*.png', '**/*.jpg'], // 包含的文件
78
+ exclude: ['**/node_modules/**'] // 排除的文件
79
+ })
80
+ ]
81
+ })
82
+ ```
83
+
84
+ ### Webpack
85
+
86
+ ```javascript
87
+ // webpack.config.js
88
+ const tinyimg = require('@pz4l/tinyimg-unplugin/webpack')
89
+
90
+ module.exports = {
91
+ plugins: [
92
+ tinyimg.default({
93
+ mode: 'random',
94
+ cache: true,
95
+ parallel: 8,
96
+ verbose: false,
97
+ strict: false,
98
+ include: ['**/*.png', '**/*.jpg'],
99
+ exclude: ['**/node_modules/**']
100
+ })
101
+ ]
102
+ }
103
+ ```
104
+
105
+ ### Rolldown
106
+
107
+ ```javascript
108
+ // rolldown.config.js
109
+ import tinyimg from '@pz4l/tinyimg-unplugin/rolldown'
110
+
111
+ export default {
112
+ plugins: [
113
+ tinyimg({
114
+ mode: 'random',
115
+ cache: true,
116
+ parallel: 8,
117
+ verbose: false,
118
+ strict: false,
119
+ include: ['**/*.png', '**/*.jpg'],
120
+ exclude: ['**/node_modules/**']
121
+ })
122
+ ]
123
+ }
124
+ ```
125
+
126
+ ## 选项
127
+
128
+ ### mode
129
+
130
+ - **类型**: `'random' | 'round-robin' | 'priority'`
131
+ - **默认值**: `'random'`
132
+ - **说明**: API key 使用策略
133
+ - `random`: 随机选择 key(默认)
134
+ - `round-robin`: 轮询使用 key
135
+ - `priority`: 优先使用额度充足的 key
136
+
137
+ ### cache
138
+
139
+ - **类型**: `boolean`
140
+ - **默认值**: `true`
141
+ - **说明**: 是否启用缓存。启用后,已压缩过的图片(基于 MD5)不会重复压缩,节省 API 额度。
142
+
143
+ ### parallel
144
+
145
+ - **类型**: `number`
146
+ - **默认值**: `8`
147
+ - **说明**: 并发压缩数量。数值越高压缩越快,但会更快消耗 API 额度。
148
+
149
+ ### verbose
150
+
151
+ - **类型**: `boolean`
152
+ - **默认值**: `false`
153
+ - **说明**: 是否输出详细日志。启用后会显示每张图片的压缩信息。
154
+
155
+ ### strict
156
+
157
+ - **类型**: `boolean`
158
+ - **默认值**: `false`
159
+ - **说明**: 严格模式。启用后,如果压缩失败会中断构建;关闭则使用原图继续构建。
160
+
161
+ ### include
162
+
163
+ - **类型**: `string | string[]`
164
+ - **默认值**: `undefined`
165
+ - **说明**: 包含的文件匹配模式。支持 glob 模式,如 `'**/*.png'`。
166
+
167
+ ### exclude
168
+
169
+ - **类型**: `string | string[]`
170
+ - **默认值**: `undefined`
171
+ - **说明**: 排除的文件匹配模式。支持 glob 模式,如 `'**/node_modules/**'`。
172
+
173
+ ## 行为
174
+
175
+ ### 仅在生产环境
176
+
177
+ 插件仅在生产构建时生效(`NODE_ENV=production` 或 Vite 的 build 模式),开发模式下不会压缩图片。
178
+
179
+ ### 支持的格式
180
+
181
+ 仅支持 TinyPNG 支持的格式:
182
+
183
+ - PNG
184
+ - JPG / JPEG
185
+
186
+ 其他格式(如 WebP、AVIF、SVG)会被忽略。
187
+
188
+ ### 仅项目缓存
189
+
190
+ unplugin 仅使用项目级缓存(`.node_modules/.tinyimg_cache/`),不使用全局缓存。这确保每个项目的缓存相互隔离。
191
+
192
+ ### 密钥管理
193
+
194
+ - 仅从 `TINYPNG_KEYS` 环境变量读取 API key
195
+ - 不支持 CLI 的 `-k` 选项
196
+ - 自动轮询多个 key,当某个 key 额度耗尽时自动切换到下一个
197
+
198
+ ## 特性
199
+
200
+ - 构建期间自动压缩图片
201
+ - 多密钥管理优化额度使用
202
+ - 基于 MD5 的智能缓存
203
+ - 并发压缩控制
204
+ - 使用 glob 模式过滤文件
205
+ - 仅在生产环境执行
206
+
207
+ ## 相关包
208
+
209
+ - [@pz4l/tinyimg-core](https://github.com/pzehrel/tinyimg/tree/main/packages/tinyimg-core) - 核心压缩库
210
+ - [@pz4l/tinyimg-cli](https://github.com/pzehrel/tinyimg/tree/main/packages/tinyimg-cli) - 图片压缩 CLI 工具
@@ -0,0 +1,22 @@
1
+ import * as unplugin from "unplugin";
2
+
3
+ //#region src/filter.d.ts
4
+ interface FilterOptions {
5
+ include?: string | string[];
6
+ exclude?: string | string[];
7
+ }
8
+ //#endregion
9
+ //#region src/options.d.ts
10
+ interface TinyimgUnpluginOptions extends FilterOptions {
11
+ mode?: 'random' | 'round-robin' | 'priority';
12
+ cache?: boolean;
13
+ parallel?: number;
14
+ strict?: boolean;
15
+ verbose?: boolean;
16
+ }
17
+ //#endregion
18
+ //#region src/index.d.ts
19
+ declare const _default: unplugin.UnpluginInstance<TinyimgUnpluginOptions, boolean>;
20
+ //#endregion
21
+ export { _default as default };
22
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/filter.ts","../src/options.ts","../src/index.ts"],"mappings":";;;UAIiB,aAAA;EACf,OAAA;EACA,OAAA;AAAA;;;UCJe,sBAAA,SAA+B,aAAA;EAC9C,IAAA;EACA,KAAA;EACA,QAAA;EACA,MAAA;EACA,OAAA;AAAA;;;cCPqD,QAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,204 @@
1
+ import { Buffer } from "node:buffer";
2
+ import process from "node:process";
3
+ import { compressImage, formatBytes, loadKeys } from "tinyimg-core";
4
+ import { createUnplugin } from "unplugin";
5
+ import path from "node:path";
6
+ import micromatch from "micromatch";
7
+ //#region src/filter.ts
8
+ const IMAGE_EXTENSIONS = new Set([
9
+ ".png",
10
+ ".jpg",
11
+ ".jpeg"
12
+ ]);
13
+ function shouldProcessImage(id, options = {}) {
14
+ const ext = path.extname(id).toLowerCase();
15
+ if (!IMAGE_EXTENSIONS.has(ext)) return false;
16
+ if (options.include) {
17
+ const includePatterns = Array.isArray(options.include) ? options.include : [options.include];
18
+ if (!micromatch.isMatch(id, includePatterns)) return false;
19
+ }
20
+ if (options.exclude) {
21
+ const excludePatterns = Array.isArray(options.exclude) ? options.exclude : [options.exclude];
22
+ if (micromatch.isMatch(id, excludePatterns)) return false;
23
+ }
24
+ return true;
25
+ }
26
+ //#endregion
27
+ //#region src/stats.ts
28
+ var CompressionStats = class {
29
+ compressedCount = 0;
30
+ cachedCount = 0;
31
+ originalSize = 0;
32
+ compressedSize = 0;
33
+ fileResults = [];
34
+ recordCompressed(path, originalSize, compressedSize) {
35
+ this.compressedCount++;
36
+ this.originalSize += originalSize;
37
+ this.compressedSize += compressedSize;
38
+ this.fileResults.push({
39
+ path,
40
+ originalSize,
41
+ compressedSize,
42
+ cached: false
43
+ });
44
+ }
45
+ recordCached(path, size) {
46
+ this.cachedCount++;
47
+ this.originalSize += size;
48
+ this.compressedSize += size;
49
+ this.fileResults.push({
50
+ path,
51
+ originalSize: size,
52
+ compressedSize: size,
53
+ cached: true
54
+ });
55
+ }
56
+ recordError(path, error) {
57
+ this.fileResults.push({
58
+ path,
59
+ originalSize: 0,
60
+ cached: false,
61
+ error
62
+ });
63
+ }
64
+ getSummary() {
65
+ return {
66
+ compressedCount: this.compressedCount,
67
+ cachedCount: this.cachedCount,
68
+ originalSize: this.originalSize,
69
+ compressedSize: this.compressedSize,
70
+ bytesSaved: this.originalSize - this.compressedSize,
71
+ fileCount: this.fileResults.length
72
+ };
73
+ }
74
+ formatSummary() {
75
+ const summary = this.getSummary();
76
+ const lines = [];
77
+ if (summary.compressedCount === 0 && summary.cachedCount > 0) lines.push(`[tinyimg] All images cached (0 compressed, ${summary.cachedCount} cached)`);
78
+ else {
79
+ lines.push(`✓ [tinyimg] Compressed ${summary.fileCount} images (${summary.cachedCount} cached, ${summary.compressedCount} compressed)`);
80
+ lines.push(`✓ [tinyimg] Saved ${formatBytes(summary.bytesSaved)} (original: ${formatBytes(summary.originalSize)} → compressed: ${formatBytes(summary.compressedSize)})`);
81
+ }
82
+ return lines;
83
+ }
84
+ getFileResults() {
85
+ return this.fileResults;
86
+ }
87
+ };
88
+ //#endregion
89
+ //#region src/logger.ts
90
+ var TinyimgLogger = class {
91
+ stats;
92
+ verbose;
93
+ strict;
94
+ constructor(options = {}) {
95
+ this.verbose = options.verbose ?? false;
96
+ this.strict = options.strict ?? false;
97
+ this.stats = new CompressionStats();
98
+ }
99
+ logCompressing(path) {
100
+ if (this.verbose) console.log(`[tinyimg] Compressing ${path}...`);
101
+ }
102
+ logCompressed(path, originalSize, compressedSize) {
103
+ this.stats.recordCompressed(path, originalSize, compressedSize);
104
+ if (this.verbose) {
105
+ const saved = ((1 - compressedSize / originalSize) * 100).toFixed(1);
106
+ console.log(`[tinyimg] ✓ Compressed: ${formatBytes(originalSize)} → ${formatBytes(compressedSize)} (${saved}% saved)`);
107
+ }
108
+ }
109
+ logCacheHit(path, size) {
110
+ this.stats.recordCached(path, size);
111
+ if (this.verbose) console.log(`[tinyimg] Cache hit: ${path}`);
112
+ }
113
+ logError(path, error) {
114
+ this.stats.recordError(path, error);
115
+ if (this.strict) console.error(`[tinyimg] ✖ Failed to compress ${path}: ${error}. Build failed.`);
116
+ else console.warn(`[tinyimg] ⚠ Failed to compress ${path}: ${error}. Using original file.`);
117
+ }
118
+ logSummary() {
119
+ this.stats.formatSummary().forEach((line) => console.log(line));
120
+ }
121
+ getStats() {
122
+ return this.stats;
123
+ }
124
+ shouldThrowOnError() {
125
+ return this.strict;
126
+ }
127
+ };
128
+ function createLogger(options = {}) {
129
+ return new TinyimgLogger(options);
130
+ }
131
+ //#endregion
132
+ //#region src/options.ts
133
+ const VALID_MODES = new Set([
134
+ "random",
135
+ "round-robin",
136
+ "priority"
137
+ ]);
138
+ function normalizeOptions(options = {}) {
139
+ if (options.mode !== void 0 && !VALID_MODES.has(options.mode)) throw new TypeError(`Invalid mode: "${options.mode}". Must be one of: random, round-robin, priority`);
140
+ if (options.parallel !== void 0 && options.parallel <= 0) throw new RangeError(`Invalid parallel: ${options.parallel}. Must be a positive number`);
141
+ return {
142
+ mode: options.mode ?? "random",
143
+ cache: options.cache ?? true,
144
+ parallel: options.parallel ?? 8,
145
+ strict: options.strict ?? false,
146
+ verbose: options.verbose ?? false,
147
+ include: options.include ? Array.isArray(options.include) ? options.include : [options.include] : void 0,
148
+ exclude: options.exclude ? Array.isArray(options.exclude) ? options.exclude : [options.exclude] : void 0
149
+ };
150
+ }
151
+ //#endregion
152
+ //#region src/index.ts
153
+ const IMAGE_REGEX = /\.(png|jpg|jpeg|gif|webp|svg)$/i;
154
+ var src_default = createUnplugin((options = {}) => {
155
+ const normalized = normalizeOptions(options);
156
+ if (loadKeys().length === 0) throw new Error("TINYPNG_KEYS environment variable is required");
157
+ const logger = createLogger({
158
+ verbose: normalized.verbose,
159
+ strict: normalized.strict
160
+ });
161
+ return {
162
+ name: "tinyimg-unplugin",
163
+ enforce: "post",
164
+ async transform(code, id) {
165
+ if (!shouldProcessImage(id, normalized)) return null;
166
+ if (!isProductionBuild(this)) return null;
167
+ const buffer = Buffer.from(code);
168
+ const relativePath = getRelativePath(id);
169
+ logger.logCompressing(relativePath);
170
+ try {
171
+ const compressed = await compressImage(buffer, {
172
+ projectCacheOnly: true,
173
+ cache: normalized.cache,
174
+ mode: normalized.mode
175
+ });
176
+ logger.logCompressed(relativePath, buffer.length, compressed.length);
177
+ return {
178
+ code: compressed,
179
+ map: null
180
+ };
181
+ } catch (error) {
182
+ logger.logError(relativePath, error.message);
183
+ if (logger.shouldThrowOnError()) throw error;
184
+ return null;
185
+ }
186
+ },
187
+ buildEnd() {
188
+ logger.logSummary();
189
+ }
190
+ };
191
+ });
192
+ function isProductionBuild(context) {
193
+ if (context?.config?.isBuild !== void 0) return context.config.isBuild;
194
+ if (context?.mode !== void 0) return context.mode === "production";
195
+ return process.env.NODE_ENV === "production";
196
+ }
197
+ function getRelativePath(id) {
198
+ const root = process.cwd();
199
+ return id.replace(root, "").replace(IMAGE_REGEX, "");
200
+ }
201
+ //#endregion
202
+ export { src_default as default };
203
+
204
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/filter.ts","../src/stats.ts","../src/logger.ts","../src/options.ts","../src/index.ts"],"sourcesContent":["import path from 'node:path'\n// @ts-expect-error - micromatch doesn't have types\nimport micromatch from 'micromatch'\n\nexport interface FilterOptions {\n include?: string | string[]\n exclude?: string | string[]\n}\n\nexport const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg'])\n\nexport function shouldProcessImage(id: string, options: FilterOptions = {}): boolean {\n // 1. Check extension first (fast path)\n const ext = path.extname(id).toLowerCase()\n if (!IMAGE_EXTENSIONS.has(ext)) {\n return false\n }\n\n // 2. Check include pattern if provided\n if (options.include) {\n const includePatterns = Array.isArray(options.include) ? options.include : [options.include]\n const isInclude = micromatch.isMatch(id, includePatterns)\n if (!isInclude)\n return false\n }\n\n // 3. Check exclude pattern if provided\n if (options.exclude) {\n const excludePatterns = Array.isArray(options.exclude) ? options.exclude : [options.exclude]\n const isExclude = micromatch.isMatch(id, excludePatterns)\n if (isExclude)\n return false\n }\n\n return true\n}\n","import { formatBytes } from 'tinyimg-core'\n\nexport interface FileResult {\n path: string\n originalSize: number\n compressedSize?: number\n cached: boolean\n error?: string\n}\n\nexport class CompressionStats {\n private compressedCount = 0\n private cachedCount = 0\n private originalSize = 0\n private compressedSize = 0\n private fileResults: FileResult[] = []\n\n recordCompressed(path: string, originalSize: number, compressedSize: number): void {\n this.compressedCount++\n this.originalSize += originalSize\n this.compressedSize += compressedSize\n this.fileResults.push({ path, originalSize, compressedSize, cached: false })\n }\n\n recordCached(path: string, size: number): void {\n this.cachedCount++\n this.originalSize += size\n this.compressedSize += size\n this.fileResults.push({ path, originalSize: size, compressedSize: size, cached: true })\n }\n\n recordError(path: string, error: string): void {\n this.fileResults.push({ path, originalSize: 0, cached: false, error })\n }\n\n getSummary() {\n return {\n compressedCount: this.compressedCount,\n cachedCount: this.cachedCount,\n originalSize: this.originalSize,\n compressedSize: this.compressedSize,\n bytesSaved: this.originalSize - this.compressedSize,\n fileCount: this.fileResults.length,\n }\n }\n\n formatSummary(): string[] {\n const summary = this.getSummary()\n const lines: string[] = []\n\n if (summary.compressedCount === 0 && summary.cachedCount > 0) {\n // All cached (D-11)\n lines.push(`[tinyimg] All images cached (0 compressed, ${summary.cachedCount} cached)`)\n }\n else {\n // Normal summary (D-09)\n lines.push(`✓ [tinyimg] Compressed ${summary.fileCount} images (${summary.cachedCount} cached, ${summary.compressedCount} compressed)`)\n lines.push(`✓ [tinyimg] Saved ${formatBytes(summary.bytesSaved)} (original: ${formatBytes(summary.originalSize)} → compressed: ${formatBytes(summary.compressedSize)})`)\n }\n\n return lines\n }\n\n getFileResults(): readonly FileResult[] {\n return this.fileResults\n }\n}\n","import { formatBytes } from 'tinyimg-core'\nimport { CompressionStats } from './stats'\n\nexport interface LoggerOptions {\n verbose?: boolean\n strict?: boolean\n}\n\nexport class TinyimgLogger {\n private stats: CompressionStats\n private verbose: boolean\n private strict: boolean\n\n constructor(options: LoggerOptions = {}) {\n this.verbose = options.verbose ?? false\n this.strict = options.strict ?? false\n this.stats = new CompressionStats()\n }\n\n logCompressing(path: string): void {\n if (this.verbose) {\n console.log(`[tinyimg] Compressing ${path}...`)\n }\n }\n\n logCompressed(path: string, originalSize: number, compressedSize: number): void {\n this.stats.recordCompressed(path, originalSize, compressedSize)\n\n if (this.verbose) {\n const saved = ((1 - compressedSize / originalSize) * 100).toFixed(1)\n console.log(`[tinyimg] ✓ Compressed: ${formatBytes(originalSize)} → ${formatBytes(compressedSize)} (${saved}% saved)`)\n }\n }\n\n logCacheHit(path: string, size: number): void {\n this.stats.recordCached(path, size)\n\n if (this.verbose) {\n console.log(`[tinyimg] Cache hit: ${path}`)\n }\n }\n\n logError(path: string, error: string): void {\n this.stats.recordError(path, error)\n\n if (this.strict) {\n // Strict mode (D-13)\n console.error(`[tinyimg] ✖ Failed to compress ${path}: ${error}. Build failed.`)\n }\n else {\n // Non-strict mode (D-12)\n console.warn(`[tinyimg] ⚠ Failed to compress ${path}: ${error}. Using original file.`)\n }\n }\n\n logSummary(): void {\n const lines = this.stats.formatSummary()\n lines.forEach(line => console.log(line))\n }\n\n getStats(): CompressionStats {\n return this.stats\n }\n\n shouldThrowOnError(): boolean {\n return this.strict\n }\n}\n\nexport function createLogger(options: LoggerOptions = {}): TinyimgLogger {\n return new TinyimgLogger(options)\n}\n","import type { FilterOptions } from './filter'\n\nexport interface TinyimgUnpluginOptions extends FilterOptions {\n mode?: 'random' | 'round-robin' | 'priority'\n cache?: boolean\n parallel?: number\n strict?: boolean\n verbose?: boolean\n}\n\nexport interface NormalizedOptions {\n mode: 'random' | 'round-robin' | 'priority'\n cache: boolean\n parallel: number\n strict: boolean\n verbose: boolean\n include?: string[]\n exclude?: string[]\n}\n\nconst VALID_MODES = new Set(['random', 'round-robin', 'priority'])\n\nexport function normalizeOptions(options: TinyimgUnpluginOptions = {}): NormalizedOptions {\n // Validate mode\n if (options.mode !== undefined && !VALID_MODES.has(options.mode)) {\n throw new TypeError(`Invalid mode: \"${options.mode}\". Must be one of: random, round-robin, priority`)\n }\n\n // Validate parallel\n if (options.parallel !== undefined && options.parallel <= 0) {\n throw new RangeError(`Invalid parallel: ${options.parallel}. Must be a positive number`)\n }\n\n return {\n mode: options.mode ?? 'random',\n cache: options.cache ?? true,\n parallel: options.parallel ?? 8,\n strict: options.strict ?? false,\n verbose: options.verbose ?? false,\n include: options.include ? (Array.isArray(options.include) ? options.include : [options.include]) : undefined,\n exclude: options.exclude ? (Array.isArray(options.exclude) ? options.exclude : [options.exclude]) : undefined,\n }\n}\n","import type { TinyimgUnpluginOptions } from './options'\nimport { Buffer } from 'node:buffer'\nimport process from 'node:process'\nimport { compressImage, loadKeys } from 'tinyimg-core'\nimport { createUnplugin } from 'unplugin'\nimport { shouldProcessImage } from './filter'\nimport { createLogger } from './logger'\nimport { normalizeOptions } from './options'\n\n// Regex for matching image file extensions\nconst IMAGE_REGEX = /\\.(png|jpg|jpeg|gif|webp|svg)$/i\n\nexport default createUnplugin((options: TinyimgUnpluginOptions = {}): any => {\n // Normalize options\n const normalized = normalizeOptions(options)\n\n // Validate TINYPNG_KEYS (D-15, D-16)\n const keys = loadKeys()\n if (keys.length === 0) {\n throw new Error('TINYPNG_KEYS environment variable is required')\n }\n\n // Create logger\n const logger = createLogger({\n verbose: normalized.verbose,\n strict: normalized.strict,\n })\n\n return {\n name: 'tinyimg-unplugin',\n enforce: 'post', // Run after other transformations (D-02)\n\n async transform(code: any, id: any) {\n // Filter non-image files\n const shouldProcess = shouldProcessImage(id, normalized)\n if (!shouldProcess) {\n return null\n }\n\n // Check production build (D-01)\n const isProd = isProductionBuild(this)\n if (!isProd) {\n return null\n }\n\n // Convert to Buffer\n const buffer = Buffer.from(code)\n\n // Get relative path for logging\n const relativePath = getRelativePath(id)\n\n // Log compression start\n logger.logCompressing(relativePath)\n\n try {\n // Compress image\n const compressed = await compressImage(buffer, {\n projectCacheOnly: true, // Only project cache (D-17)\n cache: normalized.cache,\n mode: normalized.mode as any,\n })\n\n // Log success\n logger.logCompressed(relativePath, buffer.length, compressed.length)\n\n return { code: compressed, map: null }\n }\n catch (error: any) {\n // Log error\n logger.logError(relativePath, error.message)\n\n // Check strict mode\n if (logger.shouldThrowOnError()) {\n throw error\n }\n\n // Non-strict: return null to use original file\n return null\n }\n },\n\n buildEnd() {\n logger.logSummary()\n },\n }\n})\n\n// Helper functions\nfunction isProductionBuild(context: any): boolean {\n // Vite: check config.isBuild (D-01)\n if (context?.config?.isBuild !== undefined) {\n return context.config.isBuild\n }\n\n // Webpack: check mode (D-01)\n if (context?.mode !== undefined) {\n return context.mode === 'production'\n }\n\n // Fallback: check NODE_ENV\n return process.env.NODE_ENV === 'production'\n}\n\nfunction getRelativePath(id: string): string {\n // Convert absolute path to relative for logging\n const root = process.cwd()\n return id.replace(root, '').replace(IMAGE_REGEX, '')\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@pz4l/tinyimg-unplugin",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "description": "unplugin for automatic image compression during build (Vite, Webpack, Rolldown)",
6
+ "author": "pzehrel",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/pzehrel/tinyimg/tree/main/packages/tinyimg-unplugin#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/pzehrel/tinyimg.git",
12
+ "directory": "packages/tinyimg-unplugin"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/pzehrel/tinyimg/issues"
16
+ },
17
+ "keywords": [
18
+ "tinypng",
19
+ "image",
20
+ "compression",
21
+ "compress",
22
+ "optimize",
23
+ "optimization",
24
+ "img",
25
+ "minify",
26
+ "unplugin",
27
+ "vite",
28
+ "webpack",
29
+ "rolldown",
30
+ "build",
31
+ "plugin",
32
+ "imagemin"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.mts",
37
+ "import": "./dist/index.mjs",
38
+ "require": "./dist/index.mjs"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "typecheck": "tsc --noEmit"
50
+ },
51
+ "peerDependencies": {
52
+ "vite": ">=2.0.0",
53
+ "webpack": ">=4.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "vite": {
57
+ "optional": true
58
+ },
59
+ "webpack": {
60
+ "optional": true
61
+ }
62
+ },
63
+ "dependencies": {
64
+ "micromatch": "^4.0.8",
65
+ "@pz4l/tinyimg-core": "workspace:*",
66
+ "unplugin": "^1.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "fast-glob": "^3.3.3",
70
+ "webpack": "^5.105.4",
71
+ "webpack-cli": "^7.0.2"
72
+ }
73
+ }