imageminでディレクトリ構成を維持する方法


問題

画像を圧縮する際に便利なimagemin ですが、吐き出した際にディレクトリ構成を無視してしまうという問題があります。

例えば、

img
├─pc
│ └─pc1.png
└─sp
   └─sp1.png

というディレクトリ構成でimgフォルダを指定すると、出力は

pc1.png
sp1.png

となってしまいます。
消えたpcフォルダとspフォルダ...

issueにも上がっていますね。
https://github.com/imagemin/imagemin/issues/191

issueを読む限り内部修正がもうすぐマージされそうな気配なので、
それまでのつなぎとしての処置を考えます。

解決方法

ディレクトリを精査して、再帰的にimageminをかけていきます。
(上記issueの@brothatruさんのコードを元に一部調整しています。)

const imagemin = require('imagemin')
const pngquant = require('imagemin-pngquant')
const imageminSvgo = require('imagemin-svgo')
const imageminOpting = require('imagemin-optipng')
const { lstatSync, readdirSync } = require('fs')
const paths = require('./paths')
const { join } = require('path')

/**
 * ディレクトリ構成を確認するhelper
 */
const isDirectory = (source) => lstatSync(source).isDirectory()

const getDirectories = (source) =>
  readdirSync(source)
    .map((name) => join(source, name))
    .filter(isDirectory)

const getDirectoriesRecursive = (source) => [
  source,
  ...getDirectories(source)
    .map(getDirectoriesRecursive)
    .reduce((a, b) => a.concat(b), []),
] 

/* 画像圧縮
 -------------------------------- */
;(async () => {
  let imageDirs = []
  imageDirs = imageDirs.concat(getDirectoriesRecursive(paths.srcImg))
  const regex = new RegExp('^' + paths.srcImg) //元ファイルの全ての構成をコピーしないように、除外したい先頭ファイルを指定する

  imageDirs.forEach(async (imageDir) => {
    const dir = imageDir
    await imagemin([`${dir}/*.{jpg,png,svg,gif}`], {
      destination: join(paths.buildImg, dir.replace(regex, '')),
      plugins: [
        pngquant({
          quality: [0.65, 0.8],
          speed: 1,
        }),
        imageminSvgo(),
        imageminOpting(),
      ],
    })
  })
})()

ディレクトリ構成を確認して、吐き出すときにそのパスを追加してあげています。
これで圧縮後もディレクトリ構成が維持されるようになりました。

paths.js
module.exports = {
  srcImg: 'src/img',
  dist: 'dist',
  distImg: 'dist/img',
  build: 'build',
  buildImg: 'build/img',
}

パスはこのように別ファイルで管理しています。