imagemin-power-cli
Version:
Optimize (compress) images with power using imagemin
365 lines (308 loc) • 12.4 kB
JavaScript
; // eslint-disable-line strict, lines-around-directive
const arrify = require("arrify");
const fileType = require("file-type");
const fs = require("fs-extra");
const getStdin = require("get-stdin");
const globby = require("globby");
const imagemin = require("imagemin");
const meow = require("meow");
const ora = require("ora");
const path = require("path");
const prettyBytes = require("pretty-bytes");
const replaceExt = require("replace-ext");
const stripIndent = require("strip-indent");
const os = require("os");
const createThrottle = require("async-throttle");
const cli = meow(
`
Usage
$ imagemin-power [input] [options]
$ imagemin-power <file> > <output>
$ cat <file> | imagemin-power > <output>
Input: Files(s), glob(s), or nothing to use stdin.
If an input argument is wrapped in quotation marks, it will be passed to
node-glob for cross-platform glob support. \`node_modules\` and
\`bower_components\` are always ignored. You can also pass no input and use
stdin, instead.
Options:
-c, --config Configuration for plugins, need export \`plugins\`.
-d, --cwd The current working directory in which to search. Defaults to process.cwd().
-m, --max-concurrency Sets the maximum number of instances of Imagemin that can run at once.
-p, --plugin Override the default plugins.
-o, --out-dir Output directory.
-r, --recursive Run the command recursively.
-i, --ignore-errors Not stop on errors (it works with only with <path|glob>).
-s --silent Reported only errors.
-v, --verbose Reported everything.
Examples
$ imagemin-power images/* --out-dir=build
$ imagemin-power foo.png > foo-optimized.png
$ cat foo.png | imagemin-power > foo-optimized.png
$ imagemin-power --plugins=pngquant foo.png > foo-optimized.png
`,
{
alias: {
/* eslint-disable id-length */
c: "config",
d: "cwd",
i: "ignore-errors",
o: "out-dir",
p: "plugin",
r: "recursive",
s: "silent",
v: "verbose"
/* eslint-enable id-length */
},
boolean: ["recursive", "ignore-errors", "verbose"],
string: ["config", "cwd", "out-dir"]
}
);
/* istanbul ignore if */
if (cli.input.length === 0 && process.stdin.isTTY) {
cli.showHelp();
}
const handleFile = (filepath, opts) =>
fs.readFile(filepath).then(data =>
imagemin
.buffer(data, {
plugins: opts.plugin
})
.then(buffer => {
let parentDirectory = "";
if (opts.recursive) {
parentDirectory = path.relative(
opts.cwd,
path.dirname(filepath)
);
}
const outDir = !path.isAbsolute(opts.outDir)
? path.join(process.cwd(), opts.outDir)
: path.normalize(opts.outDir);
const dest = path.join(
outDir,
parentDirectory,
path.basename(filepath)
);
const ret = {
data: buffer,
optimizedSize: buffer.length,
originalSize: data.length,
path:
fileType(buffer) && fileType(buffer).ext === "webp"
? replaceExt(dest, ".webp")
: dest
};
return fs.outputFile(ret.path, ret.data).then(() => ret);
})
);
const DEFAULT_PLUGINS = ["gifsicle", "jpegtran", "optipng", "svgo"];
const requirePlugins = plugins =>
plugins.map(plugin => {
try {
// eslint-disable-next-line global-require, import/no-dynamic-require
return require(`imagemin-${plugin}`)();
} catch (error) {
// eslint-disable-next-line no-console
console.error(
stripIndent(
`
Unknown plugin: ${plugin}
Did you forgot to install the plugin?
You can install it with:
$ npm install -g imagemin-${plugin}
${error}
`
).trim()
);
process.exit(1); // eslint-disable-line no-process-exit
}
});
const run = (input, options) => {
const opts = Object.assign(
{
config: null,
cwd: process.cwd(),
maxConcurrency: os.cpus().length,
// Info support multiple plugins
plugin: DEFAULT_PLUGINS,
recursive: false,
silent: false,
verbose: false
},
options
);
const dataSource = opts.config || path.resolve("./.imagemin.js");
if (opts.config) {
let config = null;
try {
// eslint-disable-next-line global-require, import/no-dynamic-require
config = require(path.resolve(dataSource));
} catch (error) {
console.error(`Cannot require "config"\n${error}`); // eslint-disable-line no-console
process.exit(1); // eslint-disable-line no-process-exit
}
opts.plugin = config.plugins;
} else {
opts.plugin = requirePlugins(arrify(opts.plugin));
}
if (Buffer.isBuffer(input)) {
return imagemin
.buffer(input, {
plugins: opts.plugin
})
.then(buf => process.stdout.write(buf));
}
let spinner = null;
if (opts.verbose || opts.silent) {
spinner = ora();
if (opts.verbose) {
spinner.text = "Starting minifying images...";
spinner.start();
}
}
/* istanbul ignore if */
if (!Array.isArray(input)) {
throw new TypeError("Expected an array");
}
const throttle = createThrottle(opts.maxConcurrency);
let successCounter = 0;
let failCounter = 0;
let totalBytes = 0;
let totalSavedBytes = 0;
return globby(input, {
cwd: opts.cwd,
nodir: true
})
.then(paths => {
// Maybe throw error if not found images
// eslint-disable-next-line promise/always-return
if (!opts.outDir && paths.length > 1) {
// eslint-disable-next-line no-console
console.error(
"Cannot write multiple files to stdout, specify a `--out-dir`"
);
process.exit(1); // eslint-disable-line no-process-exit
}
return Promise.all(
paths.map(filePath =>
throttle(() => {
const absoluteFilepath = !path.isAbsolute(filePath)
? path.join(opts.cwd, filePath)
: path.normalize(filePath);
const total = paths.length;
return handleFile(absoluteFilepath, opts).then(
result => {
if (opts.verbose) {
successCounter++;
// Bug in nyc :sob:
// eslint-disable-next-line prefer-destructuring
const originalSize = result.originalSize;
// eslint-disable-next-line prefer-destructuring
const optimizedSize = result.optimizedSize;
const saved = originalSize - optimizedSize;
const percent =
originalSize > 0
? saved / originalSize * 100
: 0;
const savedMsg =
`saved ${prettyBytes(saved)} ` +
`- ${percent
.toFixed(1)
.replace(/\.0$/, "")}%`;
totalBytes += originalSize;
totalSavedBytes += saved;
spinner.text =
`Minifying image "${absoluteFilepath}" ` +
`(${successCounter +
failCounter} of ${total})` +
`${saved > 0
? ` - ${savedMsg}`
: " - already optimized"}...`;
spinner.succeed();
spinner.text = "Minifying images...";
spinner.start();
}
return absoluteFilepath;
},
error => {
if (opts.ignoreErrors) {
if (opts.verbose || opts.silent) {
failCounter++;
spinner.text =
`Minifying image "${absoluteFilepath}" ` +
`(${successCounter +
failCounter} of ${total})...\nError: ${error.stack}...`;
spinner.fail();
spinner.text = "Minifying images...";
spinner.start();
}
return Promise.resolve();
}
return Promise.reject(error);
}
);
})
)
);
})
.then(files => {
if (opts.verbose) {
const percent =
totalBytes > 0 ? totalSavedBytes / totalBytes * 100 : 0;
spinner.text =
`Successfully compressed images: ${successCounter}. ` +
`Unsuccessfully compressed images: ${failCounter}. ` +
`Total images: ${successCounter + failCounter}. ` +
`Total images size: ${prettyBytes(totalBytes)}. ` +
`Total saved size: ${prettyBytes(
totalSavedBytes
)} - ${percent}%. `;
spinner.stopAndPersist("ℹ");
}
return files;
})
.catch(error => {
if (opts.verbose || opts.silent) {
spinner.fail();
}
throw error;
});
};
const optionsBase = {};
if (cli.flags.cwd) {
optionsBase.cwd = cli.flags.cwd;
}
if (cli.flags.config) {
optionsBase.config = cli.flags.config;
}
if (cli.flags.maxConcurrency) {
optionsBase.maxConcurrency = cli.flags.maxConcurrency;
}
if (cli.flags.plugin) {
optionsBase.plugin = cli.flags.plugin;
}
if (cli.flags.outDir) {
optionsBase.outDir = cli.flags.outDir;
}
if (cli.flags.verbose) {
optionsBase.verbose = cli.flags.verbose;
}
if (cli.flags.recursive) {
optionsBase.recursive = cli.flags.recursive;
}
if (cli.flags.ignoreErrors) {
optionsBase.ignoreErrors = cli.flags.ignoreErrors;
}
if (cli.input.length > 0) {
run(cli.input, cli.flags).catch(error => {
console.error(error.stack); // eslint-disable-line no-console
process.exit(1); // eslint-disable-line no-process-exit
});
} else {
getStdin.buffer().then(buf => run(buf, cli.flags)).catch(error => {
console.error(error.stack); // eslint-disable-line no-console
const exitCode = typeof error.code === "number" ? error.code : 1;
process.exit(exitCode); // eslint-disable-line no-process-exit
});
}