optimo 0.0.0 → 0.0.7
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/LICENSE.md +1 -1
- package/README.md +39 -26
- package/bin/help.js +19 -0
- package/bin/index.js +34 -37
- package/package.json +20 -13
- package/src/colors.js +20 -0
- package/src/index.js +247 -0
- package/src/util.js +24 -0
- package/.editorconfig +0 -19
- package/.gitattributes +0 -1
- package/.github/dependabot.yml +0 -11
- package/bin/help.txt +0 -13
- package/index.js +0 -3
package/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright ©
|
|
3
|
+
Copyright © 2019 Microlink <hello@microlink.io> (microlink.io)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -1,43 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<p align="center">
|
|
4
|
-
<br>
|
|
5
|
-
<img src="https://i.imgur.com/Mh13XWB.gif" alt="optimo">
|
|
1
|
+
<div align="center">
|
|
6
2
|
<br>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
<img style="width: 500px; margin:3rem 0 1.5rem;" src="https://github.com/Kikobeats/optimo/raw/master/media/banner.jpg" alt="optimo">
|
|
4
|
+
<br><br>
|
|
5
|
+
<a href="https://microlink.io"><img src="https://img.shields.io/badge/powered_by-microlink.io-blue?style=flat-square&color=%23EA407B" alt="Powered by microlink.io"></a>
|
|
6
|
+
<img alt="Last version" src="https://img.shields.io/github/tag/kikobeats/optimo.svg?style=flat-square">
|
|
7
|
+
<a href="https://www.npmjs.org/package/optimo"><img alt="NPM Status" src="https://img.shields.io/npm/dm/optimo.svg?style=flat-square"></a>
|
|
8
|
+
<br><br>
|
|
9
|
+
</div>
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
`optimo` is an CLI for aggressively reducing image file size with sane defaults. It's implemented on top of [ImageMagick](https://imagemagick.org/#gsc.tab=0).
|
|
16
12
|
|
|
17
13
|
## Install
|
|
18
14
|
|
|
19
15
|
```bash
|
|
20
|
-
|
|
16
|
+
npx -y optimo public/media # for a directory
|
|
17
|
+
npx -y optimo public/media/banner.png # for a file
|
|
18
|
+
npx -y optimo public/media/banner.png -f jpeg # convert + optimize
|
|
21
19
|
```
|
|
22
20
|
|
|
23
|
-
##
|
|
21
|
+
## Optimization Strategy
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
- Compression-first per format.
|
|
24
|
+
- Metadata stripping (`-strip`) where applicable.
|
|
25
|
+
- Format-specific tuning for stronger size reduction.
|
|
26
|
+
- Safety guard: if optimized output is not smaller, original file is kept.
|
|
27
|
+
|
|
28
|
+
## Programmatic API
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const optimo = require('optimo')
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
// optimize a single file
|
|
34
|
+
await optimo.file('/absolute/path/image.jpg', {
|
|
35
|
+
dryRun: false,
|
|
36
|
+
format: 'webp',
|
|
37
|
+
onLogs: console.log
|
|
38
|
+
})
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
// optimize a dir recursively
|
|
41
|
+
const result = await optimo.dir('/absolute/path/images')
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
console.log(result)
|
|
44
|
+
// {
|
|
45
|
+
// originalSize: Number,
|
|
46
|
+
// optimizedSize: Number,
|
|
47
|
+
// savings: Number
|
|
48
|
+
// }
|
|
36
49
|
```
|
|
37
50
|
|
|
38
51
|
## License
|
|
39
52
|
|
|
40
|
-
**optimo** © [
|
|
41
|
-
Authored and maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/
|
|
53
|
+
**optimo** © [Microlink](https://microlink.io), released under the [MIT](https://github.com/microlinkhq/optimo/blob/master/LICENSE.md) License.<br>
|
|
54
|
+
Authored and maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/microlinkhq/optimo/contributors).
|
|
42
55
|
|
|
43
|
-
> [
|
|
56
|
+
> [microlink.io](https://microlink.io) · GitHub [microlinkhq](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq)
|
package/bin/help.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { gray, blue } = require('../src/colors')
|
|
4
|
+
|
|
5
|
+
module.exports = gray(`Efortless image optimizer
|
|
6
|
+
|
|
7
|
+
Usage
|
|
8
|
+
$ ${blue('optimo')} <file|dir> [options]
|
|
9
|
+
|
|
10
|
+
Options
|
|
11
|
+
-d, --dry-run Show what would be optimized without making changes
|
|
12
|
+
-f, --format Convert output format (e.g. jpeg, webp, avif)
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
$ optimo image.jpg
|
|
16
|
+
$ optimo image.png --dry-run
|
|
17
|
+
$ optimo image.jpg -d
|
|
18
|
+
$ optimo image.png -f jpeg
|
|
19
|
+
`)
|
package/bin/index.js
CHANGED
|
@@ -1,49 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const JoyCon = require('joycon')
|
|
4
|
+
const { stat } = require('node:fs/promises')
|
|
5
|
+
const optimo = require('optimo')
|
|
7
6
|
const mri = require('mri')
|
|
8
7
|
|
|
9
|
-
require('
|
|
8
|
+
const colors = require('../src/colors')
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
async function main () {
|
|
11
|
+
const argv = mri(process.argv.slice(2), {
|
|
12
|
+
alias: {
|
|
13
|
+
'dry-run': 'd',
|
|
14
|
+
format: 'f',
|
|
15
|
+
silent: 's'
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const input = argv._[0]
|
|
20
|
+
|
|
21
|
+
if (!input) {
|
|
22
|
+
console.log(require('./help'))
|
|
23
|
+
process.exit(0)
|
|
15
24
|
}
|
|
16
|
-
})
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
const stats = await stat(input)
|
|
27
|
+
const isDirectory = stats.isDirectory()
|
|
28
|
+
const fn = isDirectory ? optimo.dir : optimo.file
|
|
29
|
+
|
|
30
|
+
const logger = argv.silent ? () => {} : logEntry => console.log(logEntry)
|
|
31
|
+
!argv.silent && console.log()
|
|
32
|
+
|
|
33
|
+
await fn(input, {
|
|
34
|
+
dryRun: argv['dry-run'],
|
|
35
|
+
format: argv.format,
|
|
36
|
+
onLogs: logger
|
|
37
|
+
})
|
|
38
|
+
|
|
20
39
|
process.exit(0)
|
|
21
40
|
}
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'package.json',
|
|
28
|
-
`.${pkg.name}rc`,
|
|
29
|
-
`.${pkg.name}rc.json`,
|
|
30
|
-
`.${pkg.name}rc.js`,
|
|
31
|
-
`${pkg.name}.config.js`
|
|
32
|
-
]
|
|
42
|
+
main().catch(error => {
|
|
43
|
+
console.error(`${colors.red('Error:')} ${error.message}`)
|
|
44
|
+
console.error(error.stack)
|
|
45
|
+
process.exit(1)
|
|
33
46
|
})
|
|
34
|
-
|
|
35
|
-
const { data: config = {} } = (await joycon.load()) || {}
|
|
36
|
-
|
|
37
|
-
Promise.resolve(
|
|
38
|
-
require('optimo')({
|
|
39
|
-
...config,
|
|
40
|
-
...flags
|
|
41
|
-
})
|
|
42
|
-
)
|
|
43
|
-
.then(() => {
|
|
44
|
-
process.exit(0)
|
|
45
|
-
})
|
|
46
|
-
.catch(error => {
|
|
47
|
-
console.error(error)
|
|
48
|
-
process.exit(1)
|
|
49
|
-
})
|
package/package.json
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
"name": "optimo",
|
|
3
3
|
"description": "The no-brainer ImageMagick CLI for optimizing images",
|
|
4
4
|
"homepage": "https://github.com/kikobeats/optimo",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.7",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"optimo": "bin/index.js"
|
|
8
11
|
},
|
|
@@ -11,6 +14,7 @@
|
|
|
11
14
|
"name": "Kiko Beats",
|
|
12
15
|
"url": "https://kikobeats.com"
|
|
13
16
|
},
|
|
17
|
+
"contributors": [],
|
|
14
18
|
"repository": {
|
|
15
19
|
"type": "git",
|
|
16
20
|
"url": "git+https://github.com/kikobeats/optimo.git"
|
|
@@ -28,7 +32,8 @@
|
|
|
28
32
|
"optimize"
|
|
29
33
|
],
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"mri": "~1.2.0"
|
|
35
|
+
"mri": "~1.2.0",
|
|
36
|
+
"tinyspawn": "~1.5.5"
|
|
32
37
|
},
|
|
33
38
|
"devDependencies": {
|
|
34
39
|
"@commitlint/cli": "latest",
|
|
@@ -36,18 +41,22 @@
|
|
|
36
41
|
"@ksmithut/prettier-standard": "latest",
|
|
37
42
|
"ava": "latest",
|
|
38
43
|
"c8": "latest",
|
|
44
|
+
"conventional-changelog-cli": "latest",
|
|
39
45
|
"finepack": "latest",
|
|
40
46
|
"git-authors-cli": "latest",
|
|
41
47
|
"github-generate-release": "latest",
|
|
42
48
|
"nano-staged": "latest",
|
|
43
49
|
"simple-git-hooks": "latest",
|
|
44
50
|
"standard": "latest",
|
|
45
|
-
"standard-markdown": "latest",
|
|
46
51
|
"standard-version": "latest"
|
|
47
52
|
},
|
|
48
53
|
"engines": {
|
|
49
54
|
"node": ">= 24"
|
|
50
55
|
},
|
|
56
|
+
"files": [
|
|
57
|
+
"bin",
|
|
58
|
+
"src"
|
|
59
|
+
],
|
|
51
60
|
"preferGlobal": true,
|
|
52
61
|
"license": "MIT",
|
|
53
62
|
"commitlint": {
|
|
@@ -60,17 +69,11 @@
|
|
|
60
69
|
]
|
|
61
70
|
}
|
|
62
71
|
},
|
|
63
|
-
"exports": {
|
|
64
|
-
".": "./cli/index.js"
|
|
65
|
-
},
|
|
66
72
|
"nano-staged": {
|
|
67
73
|
"*.js": [
|
|
68
74
|
"prettier-standard",
|
|
69
75
|
"standard --fix"
|
|
70
76
|
],
|
|
71
|
-
"*.md": [
|
|
72
|
-
"standard-markdown"
|
|
73
|
-
],
|
|
74
77
|
"package.json": [
|
|
75
78
|
"finepack"
|
|
76
79
|
]
|
|
@@ -81,14 +84,18 @@
|
|
|
81
84
|
},
|
|
82
85
|
"scripts": {
|
|
83
86
|
"clean": "rm -rf node_modules",
|
|
84
|
-
"contributors": "(
|
|
87
|
+
"contributors": "(git-authors-cli && finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
|
|
85
88
|
"coverage": "c8 report --reporter=text-lcov > coverage/lcov.info",
|
|
86
89
|
"lint": "standard-markdown README.md && standard",
|
|
87
|
-
"postrelease": "pnpm release:tags && pnpm release:github && pnpm publish",
|
|
90
|
+
"postrelease": "pnpm release:tags && pnpm release:github && pnpm publish --access=public",
|
|
88
91
|
"pretest": "pnpm lint",
|
|
89
|
-
"release": "
|
|
92
|
+
"release": "pnpm release:version && pnpm release:changelog && pnpm release:commit && pnpm release:tag",
|
|
93
|
+
"release:changelog": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -s",
|
|
94
|
+
"release:commit": "git add package.json CHANGELOG.md && git commit -m \"chore(release): $(node -p \"require('./package.json').version\")\"",
|
|
90
95
|
"release:github": "github-generate-release",
|
|
91
|
-
"release:
|
|
96
|
+
"release:tag": "git tag -a v$(node -p \"require('./package.json').version\") -m \"v$(node -p \"require('./package.json').version\")\"",
|
|
97
|
+
"release:tags": "git push origin HEAD:master --follow-tags",
|
|
98
|
+
"release:version": "standard-version --skip.changelog --skip.commit --skip.tag",
|
|
92
99
|
"test": "c8 ava"
|
|
93
100
|
}
|
|
94
101
|
}
|
package/src/colors.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { styleText } = require('node:util')
|
|
4
|
+
|
|
5
|
+
const gray = str => styleText('gray', str)
|
|
6
|
+
|
|
7
|
+
const white = str => styleText('white', str)
|
|
8
|
+
|
|
9
|
+
const green = str => styleText('green', str)
|
|
10
|
+
|
|
11
|
+
const red = str => styleText('red', str)
|
|
12
|
+
|
|
13
|
+
const yellow = str => styleText('yellow', str)
|
|
14
|
+
|
|
15
|
+
const blue = str => styleText('blue', str)
|
|
16
|
+
|
|
17
|
+
const label = (text, color) =>
|
|
18
|
+
styleText(['inverse', 'bold', color], ` ${text.toUpperCase()} `)
|
|
19
|
+
|
|
20
|
+
module.exports = { gray, white, green, red, yellow, label, blue }
|
package/src/index.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { stat, unlink, rename, readdir } = require('node:fs/promises')
|
|
4
|
+
const { execSync } = require('child_process')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const $ = require('tinyspawn')
|
|
7
|
+
|
|
8
|
+
const { formatLog, formatBytes } = require('./util')
|
|
9
|
+
const { yellow, gray, green } = require('./colors')
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* JPEG preset (compression-first):
|
|
13
|
+
* - Favor smaller output via chroma subsampling and optimized Huffman coding.
|
|
14
|
+
* - Progressive scan improves perceived loading on the web.
|
|
15
|
+
*/
|
|
16
|
+
const MAGICK_JPEG_FLAGS = [
|
|
17
|
+
'-strip',
|
|
18
|
+
'-sampling-factor',
|
|
19
|
+
'4:2:0',
|
|
20
|
+
'-define',
|
|
21
|
+
'jpeg:optimize-coding=true',
|
|
22
|
+
'-define',
|
|
23
|
+
'jpeg:dct-method=float',
|
|
24
|
+
'-quality',
|
|
25
|
+
'80',
|
|
26
|
+
'-interlace',
|
|
27
|
+
'Plane'
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* PNG preset (maximum deflate effort):
|
|
32
|
+
* - Strip metadata payloads.
|
|
33
|
+
* - Use highest compression level with explicit strategy/filter tuning.
|
|
34
|
+
*/
|
|
35
|
+
const MAGICK_PNG_FLAGS = [
|
|
36
|
+
'-strip',
|
|
37
|
+
'-define',
|
|
38
|
+
'png:compression-level=9',
|
|
39
|
+
'-define',
|
|
40
|
+
'png:compression-strategy=1',
|
|
41
|
+
'-define',
|
|
42
|
+
'png:compression-filter=5'
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* GIF preset (animation optimization):
|
|
47
|
+
* - Coalesce frames before layer optimization to maximize delta compression.
|
|
48
|
+
* - OptimizePlus is more aggressive for animated GIF size reduction.
|
|
49
|
+
*/
|
|
50
|
+
const MAGICK_GIF_FLAGS = ['-strip', '-coalesce', '-layers', 'OptimizePlus']
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* WebP preset (compression-first):
|
|
54
|
+
* - Use a strong encoder method and preserve compatibility with lossy output.
|
|
55
|
+
*/
|
|
56
|
+
const MAGICK_WEBP_FLAGS = [
|
|
57
|
+
'-strip',
|
|
58
|
+
'-define',
|
|
59
|
+
'webp:method=6',
|
|
60
|
+
'-define',
|
|
61
|
+
'webp:thread-level=1',
|
|
62
|
+
'-quality',
|
|
63
|
+
'80'
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* AVIF preset (compression-first):
|
|
68
|
+
* - Slow encoder speed for stronger compression.
|
|
69
|
+
*/
|
|
70
|
+
const MAGICK_AVIF_FLAGS = [
|
|
71
|
+
'-strip',
|
|
72
|
+
'-define',
|
|
73
|
+
'heic:speed=1',
|
|
74
|
+
'-quality',
|
|
75
|
+
'50'
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* HEIC/HEIF preset (compression-first):
|
|
80
|
+
* - Slow encoder speed for stronger compression.
|
|
81
|
+
*/
|
|
82
|
+
const MAGICK_HEIC_FLAGS = [
|
|
83
|
+
'-strip',
|
|
84
|
+
'-define',
|
|
85
|
+
'heic:speed=1',
|
|
86
|
+
'-quality',
|
|
87
|
+
'75'
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* JPEG XL preset (compression-first):
|
|
92
|
+
* - Use max effort where supported.
|
|
93
|
+
*/
|
|
94
|
+
const MAGICK_JXL_FLAGS = ['-strip', '-define', 'jxl:effort=9', '-quality', '75']
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* SVG preset:
|
|
98
|
+
* - Keep optimization minimal to avoid destructive transformations.
|
|
99
|
+
*/
|
|
100
|
+
const MAGICK_SVG_FLAGS = ['-strip']
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* Generic preset for any other format:
|
|
104
|
+
* - Keep it broadly safe across decoders while still reducing size.
|
|
105
|
+
*/
|
|
106
|
+
const MAGICK_GENERIC_FLAGS = ['-strip']
|
|
107
|
+
|
|
108
|
+
const magickPath = (() => {
|
|
109
|
+
try {
|
|
110
|
+
return execSync('which magick', {
|
|
111
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
112
|
+
})
|
|
113
|
+
.toString()
|
|
114
|
+
.trim()
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
})()
|
|
119
|
+
|
|
120
|
+
const percentage = (partial, total) =>
|
|
121
|
+
(((partial - total) / total) * 100).toFixed(1)
|
|
122
|
+
|
|
123
|
+
const normalizeFormat = format => {
|
|
124
|
+
if (!format) return null
|
|
125
|
+
|
|
126
|
+
const normalized = String(format).trim().toLowerCase().replace(/^\./, '')
|
|
127
|
+
if (normalized === 'jpg') return 'jpeg'
|
|
128
|
+
if (normalized === 'tif') return 'tiff'
|
|
129
|
+
|
|
130
|
+
return normalized
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const getOutputPath = (filePath, format) => {
|
|
134
|
+
const normalizedFormat = normalizeFormat(format)
|
|
135
|
+
if (!normalizedFormat) return filePath
|
|
136
|
+
|
|
137
|
+
const parsed = path.parse(filePath)
|
|
138
|
+
return path.join(parsed.dir, `${parsed.name}.${normalizedFormat}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const getMagickFlags = filePath => {
|
|
142
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
143
|
+
if (ext === '.jpg' || ext === '.jpeg') return MAGICK_JPEG_FLAGS
|
|
144
|
+
if (ext === '.png') return MAGICK_PNG_FLAGS
|
|
145
|
+
if (ext === '.gif') return MAGICK_GIF_FLAGS
|
|
146
|
+
if (ext === '.webp') return MAGICK_WEBP_FLAGS
|
|
147
|
+
if (ext === '.avif') return MAGICK_AVIF_FLAGS
|
|
148
|
+
if (ext === '.heic' || ext === '.heif') return MAGICK_HEIC_FLAGS
|
|
149
|
+
if (ext === '.jxl') return MAGICK_JXL_FLAGS
|
|
150
|
+
if (ext === '.svg') return MAGICK_SVG_FLAGS
|
|
151
|
+
return MAGICK_GENERIC_FLAGS
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const file = async (
|
|
155
|
+
filePath,
|
|
156
|
+
{ onLogs = () => {}, dryRun, format: outputFormat } = {}
|
|
157
|
+
) => {
|
|
158
|
+
if (!magickPath) {
|
|
159
|
+
throw new Error('ImageMagick is not installed')
|
|
160
|
+
}
|
|
161
|
+
const outputPath = getOutputPath(filePath, outputFormat)
|
|
162
|
+
const flags = getMagickFlags(outputPath)
|
|
163
|
+
|
|
164
|
+
const optimizedPath = `${outputPath}.optimized`
|
|
165
|
+
const isConverting = outputPath !== filePath
|
|
166
|
+
|
|
167
|
+
let originalSize
|
|
168
|
+
try {
|
|
169
|
+
;[originalSize] = await Promise.all([
|
|
170
|
+
(await stat(filePath)).size,
|
|
171
|
+
await $('magick', [filePath, ...flags, optimizedPath])
|
|
172
|
+
])
|
|
173
|
+
} catch {
|
|
174
|
+
onLogs(formatLog('[unsupported]', yellow, filePath))
|
|
175
|
+
return { originalSize: 0, optimizedSize: 0 }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const optimizedSize = (await stat(optimizedPath)).size
|
|
179
|
+
|
|
180
|
+
if (!isConverting && optimizedSize >= originalSize) {
|
|
181
|
+
await unlink(optimizedPath)
|
|
182
|
+
onLogs(formatLog('[optimized]', gray, filePath))
|
|
183
|
+
return { originalSize, optimizedSize: originalSize }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (dryRun) {
|
|
187
|
+
await unlink(optimizedPath)
|
|
188
|
+
} else {
|
|
189
|
+
if (isConverting) {
|
|
190
|
+
try {
|
|
191
|
+
await unlink(outputPath)
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error.code !== 'ENOENT') throw error
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
await unlink(filePath)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await rename(optimizedPath, outputPath)
|
|
200
|
+
|
|
201
|
+
if (isConverting) {
|
|
202
|
+
await unlink(filePath)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
onLogs(
|
|
207
|
+
formatLog(
|
|
208
|
+
`[${percentage(optimizedSize, originalSize)}%]`,
|
|
209
|
+
green,
|
|
210
|
+
isConverting ? `${filePath} -> ${outputPath}` : filePath
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return { originalSize, optimizedSize }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const dir = async (folderPath, opts) => {
|
|
218
|
+
const items = (await readdir(folderPath, { withFileTypes: true })).filter(
|
|
219
|
+
item => !item.name.startsWith('.')
|
|
220
|
+
)
|
|
221
|
+
let totalOriginalSize = 0
|
|
222
|
+
let totalOptimizedSize = 0
|
|
223
|
+
|
|
224
|
+
for (const item of items) {
|
|
225
|
+
const itemPath = path.join(folderPath, item.name)
|
|
226
|
+
|
|
227
|
+
if (item.isDirectory()) {
|
|
228
|
+
const subResult = await dir(itemPath, opts)
|
|
229
|
+
totalOriginalSize += subResult.originalSize
|
|
230
|
+
totalOptimizedSize += subResult.optimizedSize
|
|
231
|
+
} else {
|
|
232
|
+
const result = await file(itemPath, opts)
|
|
233
|
+
totalOriginalSize += result.originalSize
|
|
234
|
+
totalOptimizedSize += result.optimizedSize
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
originalSize: totalOriginalSize,
|
|
240
|
+
optimizedSize: totalOptimizedSize,
|
|
241
|
+
savings: totalOriginalSize - totalOptimizedSize
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports.file = file
|
|
246
|
+
module.exports.dir = dir
|
|
247
|
+
module.exports.formatBytes = formatBytes
|
package/src/util.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { gray } = require('./colors')
|
|
4
|
+
|
|
5
|
+
const MAX_STATUS_LENGTH = 13 // Length of '[unsupported]'
|
|
6
|
+
|
|
7
|
+
const formatBytes = bytes => {
|
|
8
|
+
if (bytes === 0) return '0 B'
|
|
9
|
+
const k = 1024
|
|
10
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
11
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
12
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const formatLog = (plainStatus, colorize, filePath) => {
|
|
16
|
+
const padding = MAX_STATUS_LENGTH - plainStatus.length
|
|
17
|
+
const paddedPlainStatus = plainStatus + ' '.repeat(Math.max(0, padding))
|
|
18
|
+
return `${colorize(paddedPlainStatus)} ${gray(filePath)}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
formatBytes,
|
|
23
|
+
formatLog
|
|
24
|
+
}
|
package/.editorconfig
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# https://editorconfig.org
|
|
2
|
-
|
|
3
|
-
root = true
|
|
4
|
-
|
|
5
|
-
[*]
|
|
6
|
-
indent_style = space
|
|
7
|
-
indent_size = 2
|
|
8
|
-
end_of_line = lf
|
|
9
|
-
charset = utf-8
|
|
10
|
-
trim_trailing_whitespace = true
|
|
11
|
-
insert_final_newline = true
|
|
12
|
-
max_line_length = 80
|
|
13
|
-
indent_brace_style = 1TBS
|
|
14
|
-
spaces_around_operators = true
|
|
15
|
-
quote_type = auto
|
|
16
|
-
|
|
17
|
-
[package.json]
|
|
18
|
-
indent_style = space
|
|
19
|
-
indent_size = 2
|
package/.gitattributes
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* text=auto
|
package/.github/dependabot.yml
DELETED
package/bin/help.txt
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Usage
|
|
2
|
-
$ optimo <command>[options]
|
|
3
|
-
|
|
4
|
-
Commands
|
|
5
|
-
--file Read the file
|
|
6
|
-
|
|
7
|
-
Options
|
|
8
|
-
--wait Wait for the app to exit
|
|
9
|
-
|
|
10
|
-
Examples
|
|
11
|
-
$ npm-url # Open the current package if you are over package.json path.
|
|
12
|
-
$ npm-url json-future
|
|
13
|
-
$ npm-url json-future -- 'google chrome' --incognito
|
package/index.js
DELETED