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 CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright © 2026 Kiko Beats <josefrancisco.verdu@gmail.com> (kikobeats.com)
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
- # optimo
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
- </p>
8
-
9
- ![Last version](https://img.shields.io/github/tag/kikobeats/optimo.svg?style=flat-square)
10
- [![Coverage Status](https://img.shields.io/coveralls/kikobeats/optimo.svg?style=flat-square)](https://coveralls.io/github/kikobeats/optimo)
11
- [![NPM Status](https://img.shields.io/npm/dm/optimo.svg?style=flat-square)](https://www.npmjs.org/package/optimo)
12
-
13
- **NOTE:** more badges availables in [shields.io](https://shields.io/)
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
- > The no-brainer ImageMagick CLI for optimizing images
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
- $ npm install optimo --global
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
- ## CLI
21
+ ## Optimization Strategy
24
22
 
25
- ```bash
26
- $ optimo --help
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
- Generates regular expressions that match a set of strings.
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
- Usage
31
- $ optimo [-gimuy] string1 string2 string3...
40
+ // optimize a dir recursively
41
+ const result = await optimo.dir('/absolute/path/images')
32
42
 
33
- Examples
34
- $ optimo foobar foobaz foozap fooza
35
- $ jq '.keywords' package.json | optimo
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** © [Kiko Beats](https://kikobeats.com), released under the [MIT](https://github.com/kikobeats/optimo/blob/master/LICENSE.md) License.<br>
41
- Authored and maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/kikobeats/optimo/contributors).
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
- > [kikobeats.com](https://kikobeats.com) · GitHub [Kiko Beats](https://github.com/kikobeats) · Twitter [@kikobeats](https://twitter.com/kikobeats)
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 path = require('path')
5
- const pkg = require('../package.json')
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('update-notifier')({ pkg }).notify()
8
+ const colors = require('../src/colors')
10
9
 
11
- const { _, ...flags } = mri(process.argv.slice(2), {
12
- /* https://github.com/lukeed/mri#usage< */
13
- default: {
14
- token: process.env.GH_TOKEN || process.env.GITHUB_TOKEN
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
- if (flags.help) {
19
- console.log(require('fs').readFileSync('./help.txt', 'utf8'))
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
- const joycon = new JoyCon({
24
- cwd,
25
- packageKey: pkg.name,
26
- files: [
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.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": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
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": "standard-version -a",
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:tags": "git push --follow-tags origin HEAD:master",
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
@@ -1,11 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: npm
4
- directory: '/'
5
- schedule:
6
- interval: daily
7
- - package-ecosystem: 'github-actions'
8
- directory: '/'
9
- schedule:
10
- # Check for updates to GitHub Actions every weekday
11
- interval: 'daily'
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
@@ -1,3 +0,0 @@
1
- 'use strict'
2
-
3
- module.exports = () => {}