Search code examples
npm-scriptsimagemin

is it possible to use imagemin-cli and keep the same folder structure of compressing files?


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.


Solution

  • 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.


    Solution

    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

    1. Firstly uninstall imagemin-cli as it's no longer necessary:

    $ npm un -D imagemin-cli

    1. Next install imagemin version 1.0.5 (This is an older package so may take npm longer to install than usual)

    $ npm i -D [email protected]

    1. Then install cli-glob. This will be used to specify the glob pattern to match the images for optimizing.

    $ 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)

    1. The glob \"app/src/... part of the script above uses cli-glob to match the necessary image source files.

    2. The paths are then piped to the imagemin.js utility node script.

    3. 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.

    4. 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:

    1. 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.

    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.