cantarella 1.0.0 → 1.0.1

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **批量压缩图片的命令行工具**
6
6
 
7
- 传入一个文件夹或单张图片,自动压缩覆盖,覆盖前把原图备份到 `.backup/` 目录。
7
+ 传入一个文件夹或单张图片,自动压缩覆盖,覆盖前把原图备份到备份目录(默认 `.backup/`,可用 `--backup-dir` 自定义)。
8
8
 
9
9
  [![Node.js](https://img.shields.io/badge/node-%E2%89%A522.12-brightgreen)](https://nodejs.org/)
10
10
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](./LICENSE)
@@ -16,8 +16,8 @@
16
16
 
17
17
  ## ✨ 特性
18
18
 
19
- - **两种使用方式**:不传参数进入交互问答模式;传参数则为纯 CLI 模式,可写进脚本
20
- - **安全第一**:覆盖原文件前自动备份到 `.backup/`;重压缩不会变小的文件自动跳过
19
+ - **两种使用方式**:不传参数进入交互问答模式;传参数则为纯 CLI 模式,可写进脚本;`-y` 全默认模式让裸命令也能进脚本
20
+ - **安全第一**:覆盖原文件前自动备份(目录名可用 `--backup-dir` 自定义,带撞名防护,绝不共用你已有的文件夹);重压缩不会变小的文件自动跳过
21
21
  - **`--dry` 预览**:真实编码计算压缩后大小,但不写任何文件
22
22
  - **`--report` 报告**:在目标目录生成 markdown 压缩报告
23
23
  - **格式转换**:jpeg / png / webp / avif / tiff / gif 互转,支持 heic(iPhone 照片)与 svg 输入
@@ -35,7 +35,7 @@ npm install -g cantarella
35
35
  ```bash
36
36
  git clone https://github.com/zzl110712/cantarella.git
37
37
  pnpm install
38
- pnpm build # 编译 TS 到 dist/(bin 入口指向编译产物)
38
+ pnpm build # 编译并压缩 TS 到 dist/(bin 入口指向编译产物)
39
39
  npm link # 注册全局命令 cantarella
40
40
  ```
41
41
 
@@ -52,20 +52,24 @@ cantarella compress ./photos -f webp # 全部转成 webp
52
52
  cantarella compress ./photos -w 1920 # 限制最大宽度 1920(只缩小不放大)
53
53
  cantarella compress ./photos --dry # 预览:真实计算大小但不写任何文件
54
54
  cantarella compress ./photos --report # 生成 markdown 压缩报告
55
+ cantarella compress -y # 跳过全部问答,全默认直接压当前目录(脚本友好)
56
+ cantarella compress ./photos --backup-dir bak # 备份到自定义目录 bak/ 而不是 .backup/
55
57
  ```
56
58
 
57
59
  交互模式中直接回车即采用默认值;ESC / Ctrl+C 随时取消。
58
60
 
59
61
  ## ⚙️ 选项
60
62
 
61
- | 选项 | 说明 | 默认值 |
62
- | ------------------------ | ------------------------------------------------- | ------------- |
63
- | `-q, --quality <number>` | 压缩质量 1-100(png 映射为压缩等级) | `80` |
64
- | `-f, --format <format>` | 输出格式:`jpeg` `png` `webp` `avif` `tiff` `gif` | 保持原格式 |
65
- | `-w, --max-width <px>` | 等比缩放的最大宽度,只缩小不放大 | `0`(不缩放) |
66
- | `-r, --recursive` | 递归处理子目录 | 关闭 |
67
- | `--dry` | 预览模式:真实计算压缩后大小,但不写任何文件 | 关闭 |
68
- | `--report` | 在目标目录生成 markdown 压缩报告 | 关闭 |
63
+ | 选项 | 说明 | 默认值 |
64
+ | ------------------------ | ------------------------------------------------------------------ | ------------- |
65
+ | `-q, --quality <number>` | 压缩质量 1-100(png 映射为压缩等级) | `80` |
66
+ | `-f, --format <format>` | 输出格式:`jpeg` `png` `webp` `avif` `tiff` `gif` | 保持原格式 |
67
+ | `-w, --max-width <px>` | 等比缩放的最大宽度,只缩小不放大 | `0`(不缩放) |
68
+ | `-r, --recursive` | 递归处理子目录 | 关闭 |
69
+ | `--dry` | 预览模式:真实计算压缩后大小,但不写任何文件 | 关闭 |
70
+ | `--report` | 在目标目录生成 markdown 压缩报告 | 关闭 |
71
+ | `-y, --yes` | 跳过全部问答、直接用默认值开压(裸命令可脚本化) | 关闭 |
72
+ | `--backup-dir <name>` | 备份目录名(须为单纯目录名;非空且非本工具创建的目录会被拒绝使用) | `.backup` |
69
73
 
70
74
  ## 📋 支持格式
71
75
 
@@ -109,6 +113,13 @@ pnpm build # 编译并压缩到 dist/(发布、npm link 前需要)
109
113
 
110
114
  ## 📜 版本记录
111
115
 
116
+ ### 🚀 1.0.1 · 2026-09-06
117
+
118
+ - ✨ **新增** `-V, --version`:打印版本号(从包自身的 package.json 动态读取,发版只改一处)
119
+ - ✨ **新增** `-y, --yes` 全默认模式:跳过全部问答、直接用默认值开压,`compress -y` 裸命令也能进脚本;与 `-q` 等选项自由组合(CLI 值优先,其余默认)
120
+ - ✨ **新增** `--backup-dir <name>` 自定义备份目录:目录结构原样重现,扫描时自动忽略;非法名(含路径分隔符、`..`)在参数层被拒绝
121
+ - 🛡️ **新增** 备份目录撞名防护:非空且非 cantarella 创建的自定义目录直接报错拒用(一个文件都不动),杜绝"用户同名文件被误认成已有备份、原图覆盖后最初版丢失"的数据安全问题
122
+
112
123
  ### 🎉 1.0.0 · 2026-09-04
113
124
 
114
125
  首个版本。
package/dist/bin/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #! /usr/bin/env node
2
- import{program as r}from"commander";import{myCompress as m}from"#lib/core/command";r.name("cantarella"),m(r),r.parse(process.argv);
2
+ import{createRequire as e}from"node:module";import{program as r}from"commander";import{myCompress as o}from"#lib/core/command";const{version:m}=e(import.meta.url)("#pkg");r.name("cantarella"),r.version(m),o(r),r.parse(process.argv);
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- var e={compress:{extensions:[".jpg",".jpeg",".png",".webp",".avif",".tiff",".gif",".heic",".heif",".svg"],ignoreDirs:[".backup"],defaultConcurrency:4}};export{e as default};
1
+ export default{compress:{extensions:[".jpg",".jpeg",".png",".webp",".avif",".tiff",".gif",".heic",".heif",".svg"],backupDir:".backup",ignoreDirs:[".backup"],defaultConcurrency:4}};
@@ -1 +1 @@
1
- import{InvalidArgumentError as t,Option as n}from"commander";import{compress as e}from"./compress.js";import{OUTPUT_FORMATS as i}from"./compressImage.js";function m(r){const o=Number.parseInt(r,10);if(Number.isNaN(o)||o<1||o>100)throw new t("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return o}function p(r){const o=Number.parseInt(r,10);if(Number.isNaN(o)||o<=0)throw new t("\u5FC5\u987B\u662F\u6B63\u6574\u6570");return o}const a=new n("-f, --format <format>","\u8F6C\u6362\u8F93\u51FA\u683C\u5F0F\uFF08\u9ED8\u8BA4\u4FDD\u6301\u539F\u683C\u5F0F\uFF09").choices([...i]);function d(r){r.command("compress [target]").description("\u6279\u91CF\u538B\u7F29\u56FE\u7247\uFF08\u8986\u76D6\u539F\u6587\u4EF6\u524D\u81EA\u52A8\u5907\u4EFD\u5230 .backup/ \u76EE\u5F55\uFF09").option("-q, --quality <number>","\u538B\u7F29\u8D28\u91CF 1-100\uFF0C\u9ED8\u8BA4 80\uFF08png \u6620\u5C04\u4E3A\u538B\u7F29\u7B49\u7EA7\uFF09",m,80).addOption(a).option("-w, --max-width <px>","\u7B49\u6BD4\u7F29\u653E\u7684\u6700\u5927\u5BBD\u5EA6\uFF0C\u53EA\u7F29\u5C0F\u4E0D\u653E\u5927",p).option("-r, --recursive","\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55",!1).option("--dry","\u9884\u89C8\u6A21\u5F0F\uFF1A\u771F\u5B9E\u8BA1\u7B97\u538B\u7F29\u540E\u5927\u5C0F\uFF0C\u4F46\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6",!1).option("--report","\u5728\u76EE\u6807\u76EE\u5F55\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A",!1).action(e)}export{d as myCompress};
1
+ import{InvalidArgumentError as n,Option as t}from"commander";import{compress as e}from"./compress.js";import{OUTPUT_FORMATS as i}from"./compressImage.js";function a(r){const o=Number.parseInt(r,10);if(Number.isNaN(o)||o<1||o>100)throw new n("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return o}function p(r){const o=Number.parseInt(r,10);if(Number.isNaN(o)||o<=0)throw new n("\u5FC5\u987B\u662F\u6B63\u6574\u6570");return o}function m(r){const o=r.trim();if(o===""||o==="."||o===".."||o.includes("/")||o.includes("\\"))throw new n("\u5FC5\u987B\u662F\u5355\u7EAF\u7684\u76EE\u5F55\u540D\uFF08\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u4E0D\u80FD\u5305\u542B / \\ . ..\uFF09");return o}const s=new t("-f, --format <format>","\u8F6C\u6362\u8F93\u51FA\u683C\u5F0F\uFF08\u9ED8\u8BA4\u4FDD\u6301\u539F\u683C\u5F0F\uFF09").choices([...i]);export function myCompress(r){r.command("compress [target]").description("\u6279\u91CF\u538B\u7F29\u56FE\u7247\uFF08\u8986\u76D6\u539F\u6587\u4EF6\u524D\u81EA\u52A8\u5907\u4EFD\u5230\u5907\u4EFD\u76EE\u5F55\uFF0C\u9ED8\u8BA4 .backup/\uFF09").option("-q, --quality <number>","\u538B\u7F29\u8D28\u91CF 1-100\uFF0C\u9ED8\u8BA4 80\uFF08png \u6620\u5C04\u4E3A\u538B\u7F29\u7B49\u7EA7\uFF09",a,80).addOption(s).option("-w, --max-width <px>","\u7B49\u6BD4\u7F29\u653E\u7684\u6700\u5927\u5BBD\u5EA6\uFF0C\u53EA\u7F29\u5C0F\u4E0D\u653E\u5927",p).option("-r, --recursive","\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55",!1).option("--dry","\u9884\u89C8\u6A21\u5F0F\uFF1A\u771F\u5B9E\u8BA1\u7B97\u538B\u7F29\u540E\u5927\u5C0F\uFF0C\u4F46\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6",!1).option("--report","\u5728\u76EE\u6807\u76EE\u5F55\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A",!1).option("-y, --yes","\u8DF3\u8FC7\u5168\u90E8\u95EE\u7B54\u3001\u76F4\u63A5\u7528\u9ED8\u8BA4\u503C\u5F00\u538B",!1).option("--backup-dir <name>","\u5907\u4EFD\u76EE\u5F55\u540D\uFF08\u9ED8\u8BA4 .backup\uFF09",m).action(e)}
@@ -1 +1 @@
1
- import*as r from"@clack/prompts";import F from"chalk";import O from"p-limit";import u from"node:path";import{collectImages as R,formatBytes as d,formatPercent as B,resolveRoot as S}from"#lib/utils/fs";import{accent as I}from"#lib/utils/theme";import{compressOne as P,OUTPUT_FORMATS as j}from"./compressImage.js";import{buildReport as A,writeReport as L}from"./report.js";import x from"#config";const J=async(p,i,W)=>{r.intro(I("Cantarella \u56FE\u7247\u538B\u7F29\u5DE5\u5177"));const c=p===void 0?process.cwd():u.resolve(p.trim());let g=i.recursive??!1,h=i.report??!1;if(p===void 0){const o=t=>W.getOptionValueSource(t)==="cli",s=await r.group({...o("quality")?{}:{quality:()=>r.text({message:"\u538B\u7F29\u8D28\u91CF\uFF081-100\uFF09",placeholder:"80",defaultValue:"80",validate:t=>{if(t===void 0||t.trim()==="")return;const n=Number(t);if(!Number.isInteger(n)||n<1||n>100)return"\u8BF7\u8F93\u5165 1 - 100 \u8303\u56F4\u5185\u7684\u6574\u6570"}})},...o("format")?{}:{format:()=>r.select({message:"\u8F93\u51FA\u683C\u5F0F",options:[{value:"",label:"\u4FDD\u6301\u539F\u683C\u5F0F",hint:"heic => jpeg, svg => png \u81EA\u52A8\u8F6C\u6362"},...j.map(t=>({value:t,label:t}))]})},...o("maxWidth")?{}:{maxWidth:()=>r.text({message:"\u6700\u5927\u5BBD\u5EA6 px\uFF080 = \u4E0D\u7F29\u653E\uFF09",placeholder:"0",defaultValue:"0",validate:t=>{if(t===void 0||t.trim()==="")return;const n=Number(t);if(!Number.isInteger(n)||n<0)return"\u8BF7\u8F93\u5165\u4E0D\u5C0F\u4E8E 0 \u7684\u6574\u6570"}})},...o("recursive")?{}:{recursive:()=>r.confirm({message:"\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55\uFF1F",initialValue:!1})},...o("report")?{}:{report:()=>r.confirm({message:"\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A\uFF1F",initialValue:!1})}},{onCancel:()=>{r.cancel("\u7528\u6237\u53D6\u6D88\u64CD\u4F5C"),process.exit(0)}});s.quality!==void 0&&(i.quality=Number(s.quality)),s.format!==void 0&&(i.format=s.format),s.maxWidth!==void 0&&Number(s.maxWidth)>0&&(i.maxWidth=Number(s.maxWidth)),s.recursive!==void 0&&(g=s.recursive),s.report!==void 0&&(h=s.report)}const q=i.quality??80,N=i.format===void 0||i.format===""?void 0:i.format,T=i.maxWidth,m=i.dry??!1,l=r.spinner();try{l.start("\u6B63\u5728\u626B\u63CF\u6587\u4EF6...");let o="",s=!1;try{const e=await S(c);o=e.root,s=e.isFile}catch{l.error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${c}`),process.exitCode=1;return}let t;if(s){const e=u.extname(c).toLowerCase();if(!x.compress.extensions.includes(e)){l.error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B\uFF1A${e===""?"\uFF08\u65E0\u6269\u5C55\u540D\uFF09":e}`),process.exitCode=1;return}t=[c]}else t=await R(o,g);if(t.length===0){l.stop(),r.outro("\u6CA1\u6709\u627E\u5230\u53EF\u5904\u7406\u7684\u56FE\u7247");return}const n={quality:q,format:N,maxWidth:T,dry:m},k=O(x.compress.defaultConcurrency),V=new Set(t);let y=0;l.message(`${m?"\u9884\u89C8":"\u538B\u7F29"} ${y}/${t.length}`);const f=await Promise.all(t.map(e=>k(async()=>{const a=await P(e,n,o,V);return y+=1,l.message(`${m?"\u9884\u89C8":"\u538B\u7F29"} ${y}/${t.length} ${u.basename(e)}`),a}))),b=f.filter(e=>e.status==="skipped"),$=f.filter(e=>e.status==="failed"),w=f.reduce((e,a)=>e+a.beforeBytes,0),v=f.reduce((e,a)=>e+(a.status==="failed"?a.beforeBytes:a.afterBytes),0),C=[`${m?"\u9884\u89C8":"\u538B\u7F29"}\u5B8C\u6210\uFF1A${f.length} \u4E2A\u6587\u4EF6`,`${d(w)} \u2192 ${d(v)}`,`\u8282\u7701 ${B(w,v)}`];if(b.length>0&&C.push(`\u8DF3\u8FC7 ${b.length}`),l.stop(),r.log.success(C.join(",")),$.length>0){r.log.error(`\u5931\u8D25 ${$.length} \u4E2A\uFF1A`);for(const e of $)r.log.error(`${u.relative(o,e.file)} \u2014\u2014 ${e.error??"\u672A\u89E3\u6790\u51FA\u53D1\u751F\u4E86\u4EC0\u4E48"}`);process.exitCode=1}if(m){r.log.message(F.bold("\u9884\u89C8\u660E\u7EC6\uFF08\u672A\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF1A"));for(const e of f){const a=u.relative(o,e.file);e.status==="failed"?r.log.error(`${a} \u2014\u2014 ${e.error}`):e.status==="skipped"?r.log.warn(`${a} \u4F1A\u8DF3\u8FC7\uFF08\u91CD\u538B\u7F29\u4E0D\u4F1A\u53D8\u5C0F\uFF09`):r.log.success(`${a} ${d(e.beforeBytes)} => ${d(e.afterBytes)}\uFF08\u8282\u7701 ${B(e.beforeBytes,e.afterBytes)}\uFF09`)}h&&r.log.warn("--dry \u6A21\u5F0F\u627F\u8BFA\u4E0D\u4F1A\u8F93\u51FA\u4EFB\u4F55\u6587\u4EF6\uFF0C\u5DF2\u5FFD\u7565 --report"),r.outro("\u9884\u89C8\u5B8C\u6210");return}if(h){const e={target:c,root:o,recursive:g,params:n,concurrency:x.compress.defaultConcurrency,startedAt:new Date},a=await L(o,A(e,f));r.log.info(`\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${a}`),r.outro("\u5B8C\u6210")}}catch(o){l.stop(),r.log.error(o instanceof Error?o.message:String(o)),process.exitCode=1}};export{J as compress};
1
+ import*as r from"@clack/prompts";import F from"chalk";import O from"p-limit";import d from"node:path";import{backupDirState as R,collectImages as I,formatBytes as g,formatPercent as W,markBackupDir as P,resolveRoot as j}from"#lib/utils/fs";import{accent as A}from"#lib/utils/theme";import{compressOne as L,OUTPUT_FORMATS as U}from"./compressImage.js";import{buildReport as E,writeReport as M}from"./report.js";import p from"#config";export const compress=async(y,a,q)=>{r.intro(A("Cantarella \u56FE\u7247\u538B\u7F29\u5DE5\u5177"));const m=y===void 0?process.cwd():d.resolve(y.trim());let $=a.recursive??!1,h=a.report??!1;const f=a.backupDir??p.compress.backupDir;if(y===void 0&&!a.yes){const o=t=>q.getOptionValueSource(t)==="cli",s=await r.group({...o("quality")?{}:{quality:()=>r.text({message:"\u538B\u7F29\u8D28\u91CF\uFF081-100\uFF09",placeholder:"80",defaultValue:"80",validate:t=>{if(t===void 0||t.trim()==="")return;const l=Number(t);if(!Number.isInteger(l)||l<1||l>100)return"\u8BF7\u8F93\u5165 1 - 100 \u8303\u56F4\u5185\u7684\u6574\u6570"}})},...o("format")?{}:{format:()=>r.select({message:"\u8F93\u51FA\u683C\u5F0F",options:[{value:"",label:"\u4FDD\u6301\u539F\u683C\u5F0F",hint:"heic => jpeg, svg => png \u81EA\u52A8\u8F6C\u6362"},...U.map(t=>({value:t,label:t}))]})},...o("maxWidth")?{}:{maxWidth:()=>r.text({message:"\u6700\u5927\u5BBD\u5EA6 px\uFF080 = \u4E0D\u7F29\u653E\uFF09",placeholder:"0",defaultValue:"0",validate:t=>{if(t===void 0||t.trim()==="")return;const l=Number(t);if(!Number.isInteger(l)||l<0)return"\u8BF7\u8F93\u5165\u4E0D\u5C0F\u4E8E 0 \u7684\u6574\u6570"}})},...o("recursive")?{}:{recursive:()=>r.confirm({message:"\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55\uFF1F",initialValue:!1})},...o("report")?{}:{report:()=>r.confirm({message:"\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A\uFF1F",initialValue:!1})}},{onCancel:()=>{r.cancel("\u7528\u6237\u53D6\u6D88\u64CD\u4F5C"),process.exit(0)}});s.quality!==void 0&&(a.quality=Number(s.quality)),s.format!==void 0&&(a.format=s.format),s.maxWidth!==void 0&&Number(s.maxWidth)>0&&(a.maxWidth=Number(s.maxWidth)),s.recursive!==void 0&&($=s.recursive),s.report!==void 0&&(h=s.report)}const D=a.quality??80,N=a.format===void 0||a.format===""?void 0:a.format,S=a.maxWidth,u=a.dry??!1,n=r.spinner();try{n.start("\u6B63\u5728\u626B\u63CF\u6587\u4EF6...");let o="",s=!1;try{const e=await j(m);o=e.root,s=e.isFile}catch{n.error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${m}`),process.exitCode=1;return}let t;if(s){const e=d.extname(m).toLowerCase();if(!p.compress.extensions.includes(e)){n.error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B\uFF1A${e===""?"\uFF08\u65E0\u6269\u5C55\u540D\uFF09":e}`),process.exitCode=1;return}t=[m]}else t=await I(o,$,f);if(t.length===0){n.stop(),r.outro("\u6CA1\u6709\u627E\u5230\u53EF\u5904\u7406\u7684\u56FE\u7247");return}const l=await R(o,f);if(l==="foreign"&&f!==p.compress.backupDir){n.error(`\u5907\u4EFD\u76EE\u5F55 ${f} \u5DF2\u5B58\u5728\u4E14\u5305\u542B\u975E cantarella \u521B\u5EFA\u7684\u6587\u4EF6\uFF0C\u4E3A\u907F\u514D\u8986\u76D6\u6216\u6DF7\u6DC6\u5DF2\u505C\u6B62\uFF08\u672A\u6539\u52A8\u4EFB\u4F55\u6587\u4EF6\uFF09\u3002\u8BF7\u6362\u4E00\u4E2A --backup-dir \u540D\u5B57\uFF0C\u6216\u5148\u5904\u7406\u8BE5\u76EE\u5F55`),process.exitCode=1;return}l!=="ours"&&!u&&await P(o,f);const w={quality:D,format:N,maxWidth:S,dry:u,backupDir:f},T=O(p.compress.defaultConcurrency),V=new Set(t);let b=0;n.message(`${u?"\u9884\u89C8":"\u538B\u7F29"} ${b}/${t.length}`);const c=await Promise.all(t.map(e=>T(async()=>{const i=await L(e,w,o,V);return b+=1,n.message(`${u?"\u9884\u89C8":"\u538B\u7F29"} ${b}/${t.length} ${d.basename(e)}`),i}))),k=c.filter(e=>e.status==="skipped"),x=c.filter(e=>e.status==="failed"),v=c.reduce((e,i)=>e+i.beforeBytes,0),C=c.reduce((e,i)=>e+(i.status==="failed"?i.beforeBytes:i.afterBytes),0),B=[`${u?"\u9884\u89C8":"\u538B\u7F29"}\u5B8C\u6210\uFF1A${c.length} \u4E2A\u6587\u4EF6`,`${g(v)} \u2192 ${g(C)}`,`\u8282\u7701 ${W(v,C)}`];if(k.length>0&&B.push(`\u8DF3\u8FC7 ${k.length}`),n.stop(),r.log.success(B.join(",")),x.length>0){r.log.error(`\u5931\u8D25 ${x.length} \u4E2A\uFF1A`);for(const e of x)r.log.error(`${d.relative(o,e.file)} \u2014\u2014 ${e.error??"\u672A\u89E3\u6790\u51FA\u53D1\u751F\u4E86\u4EC0\u4E48"}`);process.exitCode=1}if(u){r.log.message(F.bold("\u9884\u89C8\u660E\u7EC6\uFF08\u672A\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF1A"));for(const e of c){const i=d.relative(o,e.file);e.status==="failed"?r.log.error(`${i} \u2014\u2014 ${e.error}`):e.status==="skipped"?r.log.warn(`${i} \u4F1A\u8DF3\u8FC7\uFF08\u91CD\u538B\u7F29\u4E0D\u4F1A\u53D8\u5C0F\uFF09`):r.log.success(`${i} ${g(e.beforeBytes)} => ${g(e.afterBytes)}\uFF08\u8282\u7701 ${W(e.beforeBytes,e.afterBytes)}\uFF09`)}h&&r.log.warn("--dry \u6A21\u5F0F\u627F\u8BFA\u4E0D\u4F1A\u8F93\u51FA\u4EFB\u4F55\u6587\u4EF6\uFF0C\u5DF2\u5FFD\u7565 --report"),r.outro("\u9884\u89C8\u5B8C\u6210");return}if(h){const e={target:m,root:o,recursive:$,params:w,concurrency:p.compress.defaultConcurrency,startedAt:new Date},i=await M(o,E(e,c));r.log.info(`\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${i}`),r.outro("\u5B8C\u6210")}}catch(o){n.stop(),r.log.error(o instanceof Error?o.message:String(o)),process.exitCode=1}};
@@ -1 +1 @@
1
- import m from"sharp";import w from"path";import{randomUUID as j}from"crypto";import{readFile as v,rename as F,unlink as O,writeFile as T}from"fs/promises";import{backupFile as y,replaceExt as L}from"#lib/utils/fs";const _=["jpeg","png","webp","avif","tiff","gif"],$=["heif","svg"],l={jpeg:".jpg",png:".png",webp:".webp",avif:".avif",tiff:".tiff",gif:".gif"},x={heif:"jpeg",svg:"png"},U={".jpg":"jpeg",".jpeg":"jpeg",".png":"png",".webp":"webp",".avif":"avif",".tiff":"tiff",".gif":"gif",".heic":"heif",".heif":"heif",".svg":"svg"};function k(t){return _.includes(t)}function A(t){const r=Math.round((100-t)/100*9);return t<=60?{compressionLevel:r,palette:!0}:{compressionLevel:r}}function B(t,r,o){switch(r){case"jpeg":return t.jpeg({quality:o,mozjpeg:!0});case"png":return t.png(A(o));case"webp":return t.webp({quality:o});case"avif":return t.avif({quality:o});case"tiff":return t.tiff({quality:o});case"gif":return t.gif()}}async function I(t,r,o,d){const e={file:t,output:t,format:"jpeg",beforeBytes:0,afterBytes:0,status:"failed"};try{const n=await v(t);e.beforeBytes=n.byteLength;const p=w.extname(t).toLowerCase(),f=U[p];if(f===void 0)throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u6269\u5C55\u540D\uFF1A${p===""?"\u65E0\u6269\u5C55\u540D":p}`);const u=await m(n).metadata(),s=r.format??(k(f)?f:x[f]),c=s===f,i=c?t:L(t,l[s]);if(e.output=i,e.format=s,i!==t&&d.has(i))throw new Error(`\u8F93\u51FA ${w.basename(i)} \u548C\u5F85\u5904\u7406\u6587\u4EF6\u540C\u540D\uFF0C\u5DF2\u8DF3\u8FC7\u8F6C\u6362\uFF08\u8BE5\u6587\u4EF6\u81EA\u8EAB\u4F1A\u88AB\u5904\u7406\uFF09`);let a=m(n);r.maxWidth!==void 0&&u.width!==void 0&&u.width>r.maxWidth&&(a=a.resize({width:r.maxWidth,withoutEnlargement:!0})),s==="jpeg"&&u.hasAlpha&&a.flatten({background:"#ffffff"}),a=B(a,s,r.quality);const{data:g}=await a.toBuffer({resolveWithObject:!0});if(e.afterBytes=g.byteLength,c&&g.byteLength>=n.byteLength)return e.status="skipped",e;if(r.dry)return e.status="done",e;c&&await y(o,t);const h=`${i}${j()}.tmp`;try{await T(h,g),await F(h,i)}catch(b){throw await O(h).catch(()=>{}),b}return e.status="done",e}catch(n){return e.error=n instanceof Error?n.message:String(n),e}}export{_ as OUTPUT_FORMATS,I as compressOne};
1
+ import m from"sharp";import w from"path";import{randomUUID as j}from"crypto";import{readFile as v,rename as F,unlink as O,writeFile as T}from"fs/promises";import{backupFile as y,replaceExt as L}from"#lib/utils/fs";export const OUTPUT_FORMATS=["jpeg","png","webp","avif","tiff","gif"];const W=["heif","svg"],_={jpeg:".jpg",png:".png",webp:".webp",avif:".avif",tiff:".tiff",gif:".gif"},k={heif:"jpeg",svg:"png"},l={".jpg":"jpeg",".jpeg":"jpeg",".png":"png",".webp":"webp",".avif":"avif",".tiff":"tiff",".gif":"gif",".heic":"heif",".heif":"heif",".svg":"svg"};function x(t){return OUTPUT_FORMATS.includes(t)}function U(t){const e=Math.round((100-t)/100*9);return t<=60?{compressionLevel:e,palette:!0}:{compressionLevel:e}}function A(t,e,i){switch(e){case"jpeg":return t.jpeg({quality:i,mozjpeg:!0});case"png":return t.png(U(i));case"webp":return t.webp({quality:i});case"avif":return t.avif({quality:i});case"tiff":return t.tiff({quality:i});case"gif":return t.gif()}}export async function compressOne(t,e,i,d){const r={file:t,output:t,format:"jpeg",beforeBytes:0,afterBytes:0,status:"failed"};try{const n=await v(t);r.beforeBytes=n.byteLength;const s=w.extname(t).toLowerCase(),a=l[s];if(a===void 0)throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u6269\u5C55\u540D\uFF1A${s===""?"\u65E0\u6269\u5C55\u540D":s}`);const u=await m(n).metadata(),p=e.format??(x(a)?a:k[a]),c=p===a,o=c?t:L(t,_[p]);if(r.output=o,r.format=p,o!==t&&d.has(o))throw new Error(`\u8F93\u51FA ${w.basename(o)} \u548C\u5F85\u5904\u7406\u6587\u4EF6\u540C\u540D\uFF0C\u5DF2\u8DF3\u8FC7\u8F6C\u6362\uFF08\u8BE5\u6587\u4EF6\u81EA\u8EAB\u4F1A\u88AB\u5904\u7406\uFF09`);let f=m(n);e.maxWidth!==void 0&&u.width!==void 0&&u.width>e.maxWidth&&(f=f.resize({width:e.maxWidth,withoutEnlargement:!0})),p==="jpeg"&&u.hasAlpha&&f.flatten({background:"#ffffff"}),f=A(f,p,e.quality);const{data:g}=await f.toBuffer({resolveWithObject:!0});if(r.afterBytes=g.byteLength,c&&g.byteLength>=n.byteLength)return r.status="skipped",r;if(e.dry)return r.status="done",r;c&&await y(i,t,e.backupDir);const h=`${o}${j()}.tmp`;try{await T(h,g),await F(h,o)}catch(b){throw await O(h).catch(()=>{}),b}return r.status="done",r}catch(n){return r.error=n instanceof Error?n.message:String(n),r}}
@@ -1,2 +1,2 @@
1
- import{writeFile as d}from"fs/promises";import i from"path";import{formatBytes as u,formatPercent as $}from"#lib/utils/fs";function y(e){const s=o=>String(o).padStart(2,"0");return`${e.getFullYear()}${s(e.getMonth()+1)}${s(e.getDate())}-${s(e.getHours())}${s(e.getMinutes())}${s(e.getSeconds())}`}function w(e,s){const o=s.filter(t=>t.status==="done"),f=s.filter(t=>t.status==="skipped"),c=s.filter(t=>t.status==="failed"),p=s.reduce((t,a)=>t+a.beforeBytes,0),n=s.reduce((t,a)=>t+(a.status==="failed"?a.beforeBytes:a.afterBytes),0),r=[];r.push("# \u56FE\u7247\u538B\u7F29\u62A5\u544A",""),r.push(`- \u8FD0\u884C\u65F6\u95F4\uFF1A${e.startedAt.toLocaleString("zh-CN",{hour12:!1})}`),r.push(`- \u76EE\u6807\uFF1A${e.target}${e.recursive?"\uFF08\u9012\u5F52\uFF09":""}`),r.push(`- \u53C2\u6570\uFF1Aquality=${e.params.quality} format=${e.params.format??"\u4FDD\u6301\u539F\u683C\u5F0F"} maxWidth=${e.params.maxWidth??"\u4E0D\u7F29\u653E"} \u5E76\u53D1=${e.concurrency}${e.params.dry?"\uFF08dry \u9884\u89C8\uFF0C\u672A\u5199\u5165\uFF09":""}`,""),r.push("## \u6C47\u603B",""),r.push("| \u6307\u6807 | \u503C |","|---|---|"),r.push(`| \u5904\u7406\u6587\u4EF6\u6570 | ${s.length} |`),r.push(`| \u6210\u529F / \u8DF3\u8FC7 / \u5931\u8D25 | ${o.length} / ${f.length} / ${c.length} |`),r.push(`| \u538B\u7F29\u524D\u603B\u5927\u5C0F | ${u(p)} |`),r.push(`| \u538B\u7F29\u540E\u603B\u5927\u5C0F | ${u(n)} |`),r.push(`| \u8282\u7701 | ${u(p-n)}\uFF08${$(p,n)}\uFF09 |`,""),r.push("## \u660E\u7EC6",""),r.push("| \u6587\u4EF6 | \u8F93\u51FA\u683C\u5F0F | \u539F\u5927\u5C0F | \u538B\u7F29\u540E | \u8282\u7701 | \u72B6\u6001 |"),r.push("|---|---|---|---|---|---|");for(const t of s){const a=i.relative(e.root,t.file),h=t.status==="failed"?"-":u(t.afterBytes),l=t.status==="failed"?"-":$(t.beforeBytes,t.afterBytes),m=t.status==="failed"?`failed: ${t.error??"\u672A\u77E5\u9519\u8BEF"}`:t.status;r.push(`| ${a} | ${t.format} | ${u(t.beforeBytes)} | ${h} | ${l} | ${m} |`)}return r.push(""),r.join(`
2
- `)}async function x(e,s){const o=i.join(e,`compress-report-${y(new Date)}.md`);return await d(o,s,"utf-8"),o}export{w as buildReport,x as writeReport};
1
+ import{writeFile as d}from"fs/promises";import i from"path";import{formatBytes as u,formatPercent as $}from"#lib/utils/fs";function y(e){const s=o=>String(o).padStart(2,"0");return`${e.getFullYear()}${s(e.getMonth()+1)}${s(e.getDate())}-${s(e.getHours())}${s(e.getMinutes())}${s(e.getSeconds())}`}export function buildReport(e,s){const o=s.filter(t=>t.status==="done"),f=s.filter(t=>t.status==="skipped"),c=s.filter(t=>t.status==="failed"),p=s.reduce((t,a)=>t+a.beforeBytes,0),n=s.reduce((t,a)=>t+(a.status==="failed"?a.beforeBytes:a.afterBytes),0),r=[];r.push("# \u56FE\u7247\u538B\u7F29\u62A5\u544A",""),r.push(`- \u8FD0\u884C\u65F6\u95F4\uFF1A${e.startedAt.toLocaleString("zh-CN",{hour12:!1})}`),r.push(`- \u76EE\u6807\uFF1A${e.target}${e.recursive?"\uFF08\u9012\u5F52\uFF09":""}`),r.push(`- \u53C2\u6570\uFF1Aquality=${e.params.quality} format=${e.params.format??"\u4FDD\u6301\u539F\u683C\u5F0F"} maxWidth=${e.params.maxWidth??"\u4E0D\u7F29\u653E"} \u5E76\u53D1=${e.concurrency}${e.params.dry?"\uFF08dry \u9884\u89C8\uFF0C\u672A\u5199\u5165\uFF09":""}`,""),r.push("## \u6C47\u603B",""),r.push("| \u6307\u6807 | \u503C |","|---|---|"),r.push(`| \u5904\u7406\u6587\u4EF6\u6570 | ${s.length} |`),r.push(`| \u6210\u529F / \u8DF3\u8FC7 / \u5931\u8D25 | ${o.length} / ${f.length} / ${c.length} |`),r.push(`| \u538B\u7F29\u524D\u603B\u5927\u5C0F | ${u(p)} |`),r.push(`| \u538B\u7F29\u540E\u603B\u5927\u5C0F | ${u(n)} |`),r.push(`| \u8282\u7701 | ${u(p-n)}\uFF08${$(p,n)}\uFF09 |`,""),r.push("## \u660E\u7EC6",""),r.push("| \u6587\u4EF6 | \u8F93\u51FA\u683C\u5F0F | \u539F\u5927\u5C0F | \u538B\u7F29\u540E | \u8282\u7701 | \u72B6\u6001 |"),r.push("|---|---|---|---|---|---|");for(const t of s){const a=i.relative(e.root,t.file),h=t.status==="failed"?"-":u(t.afterBytes),l=t.status==="failed"?"-":$(t.beforeBytes,t.afterBytes),m=t.status==="failed"?`failed: ${t.error??"\u672A\u77E5\u9519\u8BEF"}`:t.status;r.push(`| ${a} | ${t.format} | ${u(t.beforeBytes)} | ${h} | ${l} | ${m} |`)}return r.push(""),r.join(`
2
+ `)}export async function writeReport(e,s){const o=i.join(e,`compress-report-${y(new Date)}.md`);return await d(o,s,"utf-8"),o}
@@ -1 +1,2 @@
1
- import{readdir as f,copyFile as m,mkdir as p,stat as l}from"fs/promises";import{constants as d}from"fs";import n from"path";import c from"#config";function y(t){if(t<1024)return`${t} B`;const e=t/1024;if(e<1024)return`${e.toFixed(2)} KB`;const r=e/1024;return r<1024?`${r.toFixed(2)} MB`:`${(r/1024).toFixed(2)} GB`}function B(t,e){return t<=0?"-":`${((1-e/t)*100).toFixed(1)}%`}async function $(t){return(await l(t)).isFile()?{root:n.dirname(t),isFile:!0}:{root:t,isFile:!1}}async function b(t,e){const r=await f(t,{withFileTypes:!0,recursive:e}),o=[];for(const i of r){if(!i.isFile())continue;const s=n.join(i.parentPath,i.name),a=n.extname(i.name).toLowerCase();c.compress.extensions.includes(a)&&(e&&n.relative(t,s).split(n.sep).some(u=>c.compress.ignoreDirs.includes(u))||o.push(s))}return o.sort()}function x(t,e){return n.join(t,".backup",n.relative(t,e))}async function P(t,e){const r=x(t,e);await p(n.dirname(r),{recursive:!0});try{return await m(e,r,d.COPYFILE_EXCL),!0}catch(o){if(o instanceof Error&&o.code==="EEXIST")return!1;throw o}}function g(t,e){const r=n.basename(t,n.extname(t));return n.join(n.dirname(t),r+e)}export{P as backupFile,x as backupPathFor,b as collectImages,y as formatBytes,B as formatPercent,g as replaceExt,$ as resolveRoot};
1
+ import{readdir as f,copyFile as w,mkdir as m,stat as c,writeFile as F}from"fs/promises";import{constants as h}from"fs";import n from"path";import i from"#config";export function formatBytes(t){if(t<1024)return`${t} B`;const r=t/1024;if(r<1024)return`${r.toFixed(2)} KB`;const e=r/1024;return e<1024?`${e.toFixed(2)} MB`:`${(e/1024).toFixed(2)} GB`}export function formatPercent(t,r){return t<=0?"-":`${((1-r/t)*100).toFixed(1)}%`}export async function resolveRoot(t){return(await c(t)).isFile()?{root:n.dirname(t),isFile:!0}:{root:t,isFile:!1}}export async function collectImages(t,r,e=i.compress.backupDir){const o=await f(t,{withFileTypes:!0,recursive:r}),s=e&&!i.compress.ignoreDirs.includes(e)?[...i.compress.ignoreDirs,e]:i.compress.ignoreDirs,u=[];for(const a of o){if(!a.isFile())continue;const p=n.join(a.parentPath,a.name),d=n.extname(a.name).toLowerCase();i.compress.extensions.includes(d)&&(r&&n.relative(t,p).split(n.sep).some(x=>s.includes(x))||u.push(p))}return u.sort()}export function backupPathFor(t,r,e=i.compress.backupDir){return n.join(t,e,n.relative(t,r))}export async function backupFile(t,r,e=i.compress.backupDir){const o=backupPathFor(t,r,e);await m(n.dirname(o),{recursive:!0});try{return await w(r,o,h.COPYFILE_EXCL),!0}catch(s){if(s instanceof Error&&s.code==="EEXIST")return!1;throw s}}const l=".cantarella-backup";export async function backupDirState(t,r){const e=n.join(t,r);try{return await c(n.join(e,l)),"ours"}catch{}let o;try{o=await c(e)}catch{return"absent"}return o.isDirectory()&&(await f(e)).length===0?"empty":"foreign"}export async function markBackupDir(t,r){const e=n.join(t,r);await m(e,{recursive:!0});try{await F(n.join(e,l),`cantarella \u5907\u4EFD\u76EE\u5F55\u6807\u8BB0\uFF1A\u672C\u76EE\u5F55\u7531 cantarella \u521B\u5EFA\u5E76\u7BA1\u7406\uFF0C\u8BF7\u52FF\u624B\u52A8\u6DF7\u5165\u5176\u4ED6\u6587\u4EF6\u3002
2
+ `,{flag:"wx"})}catch(o){if(o instanceof Error&&o.code==="EEXIST")return;throw o}}export function replaceExt(t,r){const e=n.basename(t,n.extname(t));return n.join(n.dirname(t),e+r)}
@@ -1 +1 @@
1
- import c from"chalk";const t=c.hex("#0077BE");export{t as accent};
1
+ import c from"chalk";export const accent=c.hex("#0077BE");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cantarella",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "批量压缩图片的命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,6 +49,10 @@
49
49
  "#lib/*": {
50
50
  "development": "./lib/*.ts",
51
51
  "default": "./dist/lib/*.js"
52
+ },
53
+ "#pkg": {
54
+ "development": "./package.json",
55
+ "default": "./package.json"
52
56
  }
53
57
  },
54
58
  "devDependencies": {