I'm trying to create imagemin
script with npm scripts and using imagemin-cli
for it. First, I copy files to dist
(or .tmp
for development) folder and then compress images with this scripts:
package.json
...
scripts {
"copy:dev": "cpx app/src/**/*.{html,png,jpg,mp4,webm} .tmp/",
"copy:prod": "cpx app/src/**/*.{html,png,jpg,mp4,webm} dist/",
"imagemin:dev": "imagemin app/src/images/**/* -o .tmp/images/",
"imagemin:prod": "imagemin app/src/images/**/* -o dist/images/",
...
},
So, when I run these scripts, after compression all images are put inside the folder images/
.
Is there a way to compress images and keep the folder structure? Maybe with another plugin or something else.
Is it a way to compress images with keeping folder structure?
The short answer is no, not with imagemin-cli
imagemin, (the API imagemin-cli is built upon), does not provide a mechanism to preserve the folder structure. See open issue/feature-request #191 in the projects github repo.
A cross platform way to achieve your requirements is to write a custom node.js utility script that utilizes the imagemin API directly. So effectively... build your own CLI tool that can be run via npm-scripts
.
The following gists show how this can be achieved...
imagemin.js
The utility node script is as follows:
#!/usr/bin/env node
'use strict';
var path = require('path');
var readline = require('readline');
var Imagemin = require('imagemin');
var outdir = process.env.PWD; // Default output folder.
var verbose = false; // Default no logging.
// The folder name specified MUST exist in the `glob` pattern of the npm-script.
var DEST_SUBROOT_FOLDER = 'images';
// Nice ticks for logging aren't supported via cmd.exe
var ticksymbol = process.env.npm_config_shell.indexOf('bash') !== -1 ? '✔' : '√';
var rl = readline.createInterface({
input: process.stdin,
output: null,
terminal: false
});
// Handle the optional `-o` argument for the destination folder.
if (process.argv.indexOf('-o') !== -1) {
outdir = process.argv[process.argv.indexOf('-o') + 1];
}
// Handle the optional `-v` argument for verbose logging.
if (process.argv.indexOf('-v') !== -1) {
verbose = true;
}
/**
* Utilizes the Imagemin API to create a new instance for optimizing each image.
* @param {String} srcpath - The filepath of the source image to optimize.
* @param {String} destpath - The destination path to save the resultant file.
* @param {Function} - The relevent `use` plugin (jpegtran|optipng|gifsicle).
*/
function imagemin(srcpath, destpath, plugin) {
var im = new Imagemin()
.src(srcpath)
.dest(destpath)
.use(plugin);
im.optimize(function (err, file) {
if (err) {
console.error('Error: ' + err);
process.exit(1);
}
if (file && verbose) {
console.log('\x1b[32m%s\x1b[0m', ticksymbol, destpath);
}
});
}
/**
* Obtains the destination path and file suffix from the original source path.
* @param {String} srcpath - The filepath for the image to optimize.
* @return {{dest: String, type: String}} dest path and ext (.jpg|.png|.gif).
*/
function getPathInfo(srcpath) {
var ext = path.extname(srcpath),
parts = srcpath.split(path.sep),
subpath = parts.slice(parts.indexOf(DEST_SUBROOT_FOLDER), parts.length);
subpath.unshift(outdir);
return {
dest: path.normalize(subpath.join(path.sep)),
ext: ext
};
}
/**
* Triggers the relevent imagemin process according to file suffix (jpg|png|gif).
* @param {String} srcpath - The filepath of the image to optimize.
*/
function optimizeImage(srcpath) {
var p = getPathInfo(srcpath);
switch (p.ext) {
case '.jpg':
imagemin(srcpath, p.dest, Imagemin.jpegtran({ progressive: true }));
break;
case '.png':
imagemin(srcpath, p.dest, Imagemin.optipng({ optimizationLevel: 5 }));
break;
case '.gif':
imagemin(srcpath, p.dest, Imagemin.gifsicle({ interlaced: true }));
break;
}
}
// Read each line from process.stdin (i.e. the filepath)
rl.on('line', function(srcpath) {
optimizeImage(srcpath);
});
Note: The code above uses version 1.0.5
of the imagemin
API and not the latest version - Why? See point 1 under the Additional Notes section below.)
Uninstall and Install new packages
imagemin-cli
as it's no longer necessary:$ npm un -D imagemin-cli
1.0.5
(This is an older package so may take npm
longer to install than usual)$ npm i -D imagemin@1.0.5
$ npm i -D cli-glob
npm-scripts
Update your npm-scripts
as follows:
...
"scripts": {
"imagemin:prod": "glob \"app/src/images/**/*.{png,jpg,gif}\" | node bin/imagemin -v -o dist",
"imagemin:dev": "glob \"app/src/images/**/*.{png,jpg,gif}\" | node bin/imagemin -v -o .tmp",
...
},
...
Note: To optimize images using the gists shown above it's not necessary to use the two scripts named copy:prod
and copy:dev
shown in your original post/question)
The glob \"app/src/...
part of the script above uses cli-glob to match the necessary image source files.
The paths are then piped to the imagemin.js
utility node script.
When the -v
(verbose) argument/flag is included then each processed image is logged to the console. To omit logging simply remove the -v
flag.
The -o
(output) argument/flag is used to specify the destination folder name. E.g. dist
or .tmp
. When the value for -o
is omitted the resultant images are output to the project root directory.
Additional notes:
The reason for using imagemin
version 1.0.5 is because this API
allows the src
value to be specified as a single filepath. In versions greater than 2.0.0
the API expects the src
value to be a glob pattern as shown in the latest version 5.2.2.
The gists above assume imagemin.js
is saved to a folder named bin
which exists in the same folder as package.json
. It can be changed to a preferred name, or an invisible folder by prefixing it with a dot [.] e.g. .scripts
or .bin
. Whatever you choose, you'll need to update the path to the script in npm-scripts
accordingly.