Repository: webpack-contrib/copy-webpack-plugin (js)

Issue:
cacheDir parameter?
I use cache feature.
I had to change the cache directory other than the default one; `/node_modules/.cache`
There is a check in the source code for `globalRef.cacheDir` but I am not sure how to enable this?

I tried to add `cacheDir` param to webpack config but It didnt't work.

For now I solved my problem by modifying `postProcessPattern`.

Is it possible to use a custom cache directory path without modifying the source code?

* Operating System: Windows 
* Node Version: v11.11.0
* NPM Version: 6.7.0
* webpack Version: 3.10.0
* copy-webpack-plugin Version: 4.3.1



Pull request description:
Issue 370
<!--
  HOLY CRAP a Pull Request. We ❤️ those!

  If you remove or skip this template, you'll make the 🐼 sad and the mighty god
  of Github will appear and pile-drive the close button from a great height
  while making animal noises.

  Please place an x (no spaces!) in all [ ] that apply
-->

This PR contains a:

- [ ] **bugfix**
- [x] new **feature**
- [x] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

fixes #370 fixes #432

### Breaking Changes

Yes, for the `cacheTransfrom` option

### Additional Info

No

Patch:
diff --git a/README.md b/README.md
index 0b3a8bf..a78b5db 100644
--- a/README.md
+++ b/README.md
@@ -74,19 +74,19 @@ module.exports = {
 
 ### Patterns
 
-|                  Name                   |        Type         |                     Default                     | Description                                                                                           |
-| :-------------------------------------: | :-----------------: | :---------------------------------------------: | :---------------------------------------------------------------------------------------------------- |
-|             [`from`](#from)             |     `{String}`      |                   `undefined`                   | Glob or path from where we сopy files.                                                                |
-|               [`to`](#to)               |     `{String}`      |            `compiler.options.output`            | Output path.                                                                                          |
-|          [`context`](#context)          |     `{String}`      | `options.context \|\| compiler.options.context` | A path that determines how to interpret the `from` path.                                              |
-|      [`globOptions`](#globoptions)      |     `{Object}`      |                   `undefined`                   | [Options][glob-options] passed to the glob pattern matching library including `ignore` option.        |
-|           [`toType`](#totype)           |     `{String}`      |                   `undefined`                   | Determinate what is `to` option - directory, file or template.                                        |
-|            [`force`](#force)            |     `{Boolean}`     |                     `false`                     | Overwrites files already in `compilation.assets` (usually added by other plugins/loaders).            |
-|          [`flatten`](#flatten)          |     `{Boolean}`     |                     `false`                     | Removes all directory references and only copies file names.                                          |
-|        [`transform`](#transform)        |    `{Function}`     |                   `undefined`                   | Allows to modify the file contents.                                                                   |
-|   [`cacheTransform`](#cacheTransform)   | `{Boolean\|Object}` |                     `false`                     | Enable `transform` caching. You can use `{ cache: { key: 'my-cache-key' } }` to invalidate the cache. |
-|    [`transformPath`](#transformpath)    |    `{Function}`     |                   `undefined`                   | Allows to modify the writing path.                                                                    |
-| [`noErrorOnMissing`](#noerroronmissing) |     `{Boolean}`     |                     `false`                     | Doesn't generate an error on missing file(s).                                                         |
+|                  Name                   |            Type             |                     Default                     | Description                                                                                           |
+| :-------------------------------------: | :-------------------------: | :---------------------------------------------: | :---------------------------------------------------------------------------------------------------- |
+|             [`from`](#from)             |         `{String}`          |                   `undefined`                   | Glob or path from where we сopy files.                                                                |
+|               [`to`](#to)               |         `{String}`          |            `compiler.options.output`            | Output path.                                                                                          |
+|          [`context`](#context)          |         `{String}`          | `options.context \|\| compiler.options.context` | A path that determines how to interpret the `from` path.                                              |
+|      [`globOptions`](#globoptions)      |         `{Object}`          |                   `undefined`                   | [Options][glob-options] passed to the glob pattern matching library including `ignore` option.        |
+|           [`toType`](#totype)           |         `{String}`          |                   `undefined`                   | Determinate what is `to` option - directory, file or template.                                        |
+|            [`force`](#force)            |         `{Boolean}`         |                     `false`                     | Overwrites files already in `compilation.assets` (usually added by other plugins/loaders).            |
+|          [`flatten`](#flatten)          |         `{Boolean}`         |                     `false`                     | Removes all directory references and only copies file names.                                          |
+|        [`transform`](#transform)        |        `{Function}`         |                   `undefined`                   | Allows to modify the file contents.                                                                   |
+|   [`cacheTransform`](#cacheTransform)   | `{Boolean\|String\|Object}` |                     `false`                     | Enable `transform` caching. You can use `{ cache: { key: 'my-cache-key' } }` to invalidate the cache. |
+|    [`transformPath`](#transformpath)    |        `{Function}`         |                   `undefined`                   | Allows to modify the writing path.                                                                    |
+| [`noErrorOnMissing`](#noerroronmissing) |         `{Boolean}`         |                     `false`                     | Doesn't generate an error on missing file(s).                                                         |
 
 #### `from`
 
@@ -456,12 +456,16 @@ module.exports = {
 
 #### `cacheTransform`
 
-Type: `Boolean|Object`
+Type: `Boolean|String|Object`
 Default: `false`
 
-Enable/disable `transform` caching. You can use `{ cacheTransform: { key: 'my-cache-key' } }` to invalidate the cache.
+Enable/disable and configure caching.
 Default path to cache directory: `node_modules/.cache/copy-webpack-plugin`.
 
+##### `Boolean`
+
+Enables/Disable `transform` caching.
+
 **webpack.config.js**
 
 ```js
@@ -483,6 +487,132 @@ module.exports = {
 };
 ```
 
+##### `String`
+
+Enables `transform` caching and setup cache directory.
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transform(content, path) {
+            return optimize(content);
+          },
+          // Should be absolute
+          cacheTransform: path.resolve(__dirname, 'cache-directory'),
+        },
+      ],
+    }),
+  ],
+};
+```
+
+##### `Object`
+
+Enables `transform` caching and setup cache directory and invalidation keys.
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transform(content, path) {
+            return optimize(content);
+          },
+          cacheTransform: {
+            directory: path.resolve(__dirname, 'cache-directory'),
+            keys: {
+              // May be useful for invalidating cache based on external values
+              // For example, you can invalid cache based on `process.version` - { node: process.version }
+              key: 'value',
+            },
+          },
+        },
+      ],
+    }),
+  ],
+};
+```
+
+You can setup invalidation keys using a function.
+
+Simple function:
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transform(content, path) {
+            return optimize(content);
+          },
+          cacheTransform: {
+            directory: path.resolve(__dirname, 'cache-directory'),
+            keys: (defaultCacheKeys, absoluteFrom) => {
+              const keys = getCustomCacheInvalidationKeysSync();
+
+              return {
+                ...defaultCacheKeys,
+                keys,
+              };
+            },
+          },
+        },
+      ],
+    }),
+  ],
+};
+```
+
+Async function:
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transform(content, path) {
+            return optimize(content);
+          },
+          cacheTransform: {
+            directory: path.resolve(__dirname, 'cache-directory'),
+            keys: async (defaultCacheKeys, absoluteFrom) => {
+              const keys = await getCustomCacheInvalidationKeysAsync();
+
+              return {
+                ...defaultCacheKeys,
+                keys,
+              };
+            },
+          },
+        },
+      ],
+    }),
+  ],
+};
+```
+
 #### `transformPath`
 
 Type: `Function`
diff --git a/src/options.json b/src/options.json
index aa94941..1b19cd9 100644
--- a/src/options.json
+++ b/src/options.json
@@ -35,7 +35,28 @@
               "type": "boolean"
             },
             {
-              "type": "object"
+              "type": "string"
+            },
+            {
+              "type": "object",
+              "additionalProperties": false,
+              "properties": {
+                "directory": {
+                  "type": "string",
+                  "absolutePath": true
+                },
+                "keys": {
+                  "anyOf": [
+                    {
+                      "type": "object",
+                      "additionalProperties": true
+                    },
+                    {
+                      "instanceof": "Function"
+                    }
+                  ]
+                }
+              }
             }
           ]
         },
diff --git a/src/postProcessPattern.js b/src/postProcessPattern.js
index 6f33e6a..199da4c 100644
--- a/src/postProcessPattern.js
+++ b/src/postProcessPattern.js
@@ -10,7 +10,7 @@ import normalizePath from 'normalize-path';
 
 import { RawSource } from 'webpack-sources';
 
-import { name, version } from '../package.json';
+import { version } from '../package.json';
 
 import { stat, readFile } from './utils/promisify';
 
@@ -63,22 +63,33 @@ export default async function postProcessPattern(globalRef, pattern, file) {
     logger.log(`transforming content for '${file.absoluteFrom}'`);
 
     if (pattern.cacheTransform) {
-      if (!globalRef.cacheDir) {
-        globalRef.cacheDir =
-          findCacheDir({ name: 'copy-webpack-plugin' }) || os.tmpdir();
+      const cacheDirectory = pattern.cacheTransform.directory
+        ? pattern.cacheTransform.directory
+        : typeof pattern.cacheTransform === 'string'
+        ? pattern.cacheTransform
+        : findCacheDir({ name: 'copy-webpack-plugin' }) || os.tmpdir();
+      let defaultCacheKeys = {
+        version,
+        transform: pattern.transform,
+        contentHash: crypto.createHash('md4').update(content).digest('hex'),
+      };
+
+      if (typeof pattern.cacheTransform.keys === 'function') {
+        defaultCacheKeys = await pattern.cacheTransform.keys(
+          defaultCacheKeys,
+          file.absoluteFrom
+        );
+      } else {
+        defaultCacheKeys = {
+          ...defaultCacheKeys,
+          ...pattern.cacheTransform.keys,
+        };
       }
 
-      const cacheKey = pattern.cacheTransform.key
-        ? pattern.cacheTransform.key
-        : serialize({
-            name,
-            version,
-            pattern,
-            hash: crypto.createHash('md4').update(content).digest('hex'),
-          });
+      const cacheKeys = serialize(defaultCacheKeys);
 
       try {
-        const result = await cacache.get(globalRef.cacheDir, cacheKey);
+        const result = await cacache.get(cacheDirectory, cacheKeys);
 
         logger.debug(
           `getting cached transformation for '${file.absoluteFrom}'`
@@ -91,7 +102,7 @@ export default async function postProcessPattern(globalRef, pattern, file) {
         logger.debug(`caching transformation for '${file.absoluteFrom}'`);
 
         content = await cacache
-          .put(globalRef.cacheDir, cacheKey, content)
+          .put(cacheDirectory, cacheKeys, content)
           .then(() => content);
       }
     } else {
@@ -138,6 +149,7 @@ export default async function postProcessPattern(globalRef, pattern, file) {
   const source = new RawSource(content);
 
   // For old version webpack 4
+  /* istanbul ignore if */
   if (typeof compilation.emitAsset !== 'function') {
     compilation.assets[targetPath] = source;
 
@@ -145,7 +157,7 @@ export default async function postProcessPattern(globalRef, pattern, file) {
   }
 
   if (compilation.getAsset(targetPath)) {
-    if (file.force) {
+    if (pattern.force) {
       logger.log(
         `force updating '${file.webpackTo}' to compilation assets from '${file.absoluteFrom}'`
       );
diff --git a/src/processPattern.js b/src/processPattern.js
index 83f571c..070ef75 100644
--- a/src/processPattern.js
+++ b/src/processPattern.js
@@ -32,10 +32,7 @@ export default async function processPattern(globalRef, pattern) {
   }
 
   return paths.map((from) => {
-    const file = {
-      force: pattern.force,
-      absoluteFrom: path.resolve(pattern.context, from),
-    };
+    const file = { absoluteFrom: path.resolve(pattern.context, from) };
 
     file.relativeFrom = path.relative(pattern.context, file.absoluteFrom);
 
diff --git a/src/utils/createPatternGlob.js b/src/utils/createPatternGlob.js
index ead002e..1fe84a0 100644
--- a/src/utils/createPatternGlob.js
+++ b/src/utils/createPatternGlob.js
@@ -17,13 +17,13 @@ function createPatternGlob(pattern, globalRef) {
   const { logger, compilation } = globalRef;
 
   // eslint-disable-next-line no-param-reassign
-  pattern.globOptions = Object.assign(
-    {
+  pattern.globOptions = {
+    ...{
       cwd: pattern.context,
       followSymbolicLinks: true,
     },
-    pattern.globOptions || {}
-  );
+    ...(pattern.globOptions || {}),
+  };
 
   switch (pattern.fromType) {
     case 'dir':


Test patch:
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index ef01331..cc21b05 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -37,6 +37,12 @@ exports[`validate options should throw an error on the "patterns" option with "[
  - options.patterns[0].from should be an non-empty string."
 `;
 
+exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","cacheTransform":{"foo":"bar"}}]" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options.patterns[0].cacheTransform has an unknown property 'foo'. These properties are valid:
+   object { directory?, keys? }"
+`;
+
 exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","flatten":"true"}]" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
  - options.patterns[0].flatten should be a boolean."
@@ -69,11 +75,12 @@ exports[`validate options should throw an error on the "patterns" option with "[
    non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }
    Details:
     * options.patterns[0].cacheTransform should be one of these:
-      boolean | object { … }
+      boolean | string | object { directory?, keys? }
       Details:
        * options.patterns[0].cacheTransform should be a boolean.
+       * options.patterns[0].cacheTransform should be a string.
        * options.patterns[0].cacheTransform should be an object:
-         object { … }"
+         object { directory?, keys? }"
 `;
 
 exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":true}]" value 1`] = `
diff --git a/test/cacheTransform-option.test.js b/test/cacheTransform-option.test.js
index 5b66974..5d40549 100644
--- a/test/cacheTransform-option.test.js
+++ b/test/cacheTransform-option.test.js
@@ -11,9 +11,19 @@ import { runEmit } from './helpers/run';
 const FIXTURES_DIR = path.join(__dirname, 'fixtures');
 
 describe('cache option', () => {
-  const cacheDir = findCacheDir({ name: 'copy-webpack-plugin' });
-
-  beforeEach(() => cacache.rm.all(cacheDir));
+  const defaultCacheDir = findCacheDir({ name: 'copy-webpack-plugin' });
+  const cacheDir1 = findCacheDir({ name: 'copy-webpack-plugin-1' });
+  const cacheDir2 = findCacheDir({ name: 'copy-webpack-plugin-2' });
+  const cacheDir3 = findCacheDir({ name: 'copy-webpack-plugin-3' });
+  const cacheDir4 = findCacheDir({ name: 'copy-webpack-plugin-4' });
+
+  beforeEach(() => [
+    cacache.rm.all(defaultCacheDir),
+    cacache.rm.all(cacheDir1),
+    cacache.rm.all(cacheDir2),
+    cacache.rm.all(cacheDir3),
+    cacache.rm.all(cacheDir4),
+  ]);
 
   it('should cache when "from" is a file', (done) => {
     const newContent = 'newchanged!';
@@ -37,19 +47,10 @@ describe('cache option', () => {
       ],
     })
       .then(() =>
-        cacache.ls(cacheDir).then((cacheEntries) => {
+        cacache.ls(defaultCacheDir).then((cacheEntries) => {
           const cacheKeys = Object.keys(cacheEntries);
 
           expect(cacheKeys).toHaveLength(1);
-
-          cacheKeys.forEach((cacheKey) => {
-            // eslint-disable-next-line no-new-func
-            const cacheEntry = new Function(
-              `'use strict'\nreturn ${cacheKey}`
-            )();
-
-            expect(cacheEntry.pattern.from).toBe(from);
-          });
         })
       )
       .then(done)
@@ -84,19 +85,10 @@ describe('cache option', () => {
       ],
     })
       .then(() =>
-        cacache.ls(cacheDir).then((cacheEntries) => {
+        cacache.ls(defaultCacheDir).then((cacheEntries) => {
           const cacheKeys = Object.keys(cacheEntries);
 
           expect(cacheKeys).toHaveLength(3);
-
-          cacheKeys.forEach((cacheKey) => {
-            // eslint-disable-next-line no-new-func
-            const cacheEntry = new Function(
-              `'use strict'\nreturn ${cacheKey}`
-            )();
-
-            expect(cacheEntry.pattern.from).toBe(from);
-          });
         })
       )
       .then(done)
@@ -124,7 +116,109 @@ describe('cache option', () => {
       ],
     })
       .then(() =>
-        cacache.ls(cacheDir).then((cacheEntries) => {
+        cacache.ls(defaultCacheDir).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+
+  it('should cache file with custom cache directory', (done) => {
+    const newContent = 'newchanged!';
+    const from = 'file.txt';
+
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': newContent,
+      },
+      patterns: [
+        {
+          from,
+          cacheTransform: cacheDir1,
+          transform: (content) => {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir1).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+
+  it('should cache file with custom cache directory when "cacheTransform" is an object', (done) => {
+    const newContent = 'newchanged!';
+    const from = 'file.txt';
+
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': newContent,
+      },
+      patterns: [
+        {
+          from,
+          cacheTransform: {
+            directory: cacheDir2,
+          },
+          transform: (content) => {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir2).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
+
+  it('should cache file with custom object cache keys when "cacheTransform" is an object', (done) => {
+    const newContent = 'newchanged!';
+    const from = 'file.txt';
+
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': newContent,
+      },
+      patterns: [
+        {
+          from,
+          cacheTransform: {
+            keys: {
+              key: 'foobar',
+            },
+          },
+          transform: (content) => {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(defaultCacheDir).then((cacheEntries) => {
           const cacheKeys = Object.keys(cacheEntries);
 
           expect(cacheKeys).toHaveLength(1);
@@ -132,11 +226,11 @@ describe('cache option', () => {
           cacheKeys.forEach((cacheKey) => {
             // eslint-disable-next-line no-new-func
             const cacheEntry = new Function(
-              `'use strict'\nreturn ${cacheKey}`
+              `'use strict'\nreturn (${cacheKey});`
             )();
 
-            // Todo need investigate
-            expect(cacheEntry.pattern.from.replace(/\\/, '/')).toBe(from);
+            // expect(cacheEntry.pattern.from).toBe(from);
+            expect(cacheEntry.key).toBe('foobar');
           });
         })
       )
@@ -144,7 +238,7 @@ describe('cache option', () => {
       .catch(done);
   });
 
-  it('should cache file with custom cache key', (done) => {
+  it('should cache file with custom function cache keys when "cacheTransform" is an object', (done) => {
     const newContent = 'newchanged!';
     const from = 'file.txt';
 
@@ -157,9 +251,16 @@ describe('cache option', () => {
         {
           from,
           cacheTransform: {
-            key: 'foobar',
+            keys: (defaultCacheKeys, absoluteFrom) => {
+              expect(absoluteFrom).toBeDefined();
+
+              return {
+                ...defaultCacheKeys,
+                key: 'foobar',
+              };
+            },
           },
-          transform(content) {
+          transform: (content) => {
             return new Promise((resolve) => {
               resolve(`${content}changed!`);
             });
@@ -168,13 +269,19 @@ describe('cache option', () => {
       ],
     })
       .then(() =>
-        cacache.ls(cacheDir).then((cacheEntries) => {
+        cacache.ls(defaultCacheDir).then((cacheEntries) => {
           const cacheKeys = Object.keys(cacheEntries);
 
           expect(cacheKeys).toHaveLength(1);
 
           cacheKeys.forEach((cacheKey) => {
-            expect(cacheKey).toBe('foobar');
+            // eslint-disable-next-line no-new-func
+            const cacheEntry = new Function(
+              `'use strict'\nreturn (${cacheKey});`
+            )();
+
+            // expect(cacheEntry.pattern.from).toBe(from);
+            expect(cacheEntry.key).toBe('foobar');
           });
         })
       )
@@ -182,54 +289,38 @@ describe('cache option', () => {
       .catch(done);
   });
 
-  it('should cache binary file', (done) => {
-    const from = 'file.txt.gz';
-    const content = fs.readFileSync(path.join(FIXTURES_DIR, from));
-    const expectedNewContent = zlib.gzipSync('newchanged!');
-
-    expect(isGzip(content)).toBe(true);
-    expect(isGzip(expectedNewContent)).toBe(true);
+  it('should cache file with custom async function cache keys when "cacheTransform" is an object', (done) => {
+    const newContent = 'newchanged!';
+    const from = 'file.txt';
 
     runEmit({
-      expectedAssetKeys: ['file.txt.gz'],
+      expectedAssetKeys: ['file.txt'],
       expectedAssetContent: {
-        'file.txt.gz': expectedNewContent,
+        'file.txt': newContent,
       },
       patterns: [
         {
           from,
-          cacheTransform: true,
-          // eslint-disable-next-line no-shadow
-          transform: function transform(content) {
-            expect(isGzip(content)).toBe(true);
-
+          cacheTransform: {
+            keys: async (defaultCacheKeys, absoluteFrom) => {
+              expect(absoluteFrom).toBeDefined();
+
+              return {
+                ...defaultCacheKeys,
+                key: 'foobar',
+              };
+            },
+          },
+          transform: (content) => {
             return new Promise((resolve) => {
-              // eslint-disable-next-line no-shadow
-              zlib.unzip(content, (error, content) => {
-                if (error) {
-                  throw error;
-                }
-
-                const newContent = Buffer.from(`${content}changed!`);
-
-                // eslint-disable-next-line no-shadow
-                zlib.gzip(newContent, (error, compressedData) => {
-                  if (error) {
-                    throw error;
-                  }
-
-                  expect(isGzip(compressedData)).toBe(true);
-
-                  return resolve(compressedData);
-                });
-              });
+              resolve(`${content}changed!`);
             });
           },
         },
       ],
     })
       .then(() =>
-        cacache.ls(cacheDir).then((cacheEntries) => {
+        cacache.ls(defaultCacheDir).then((cacheEntries) => {
           const cacheKeys = Object.keys(cacheEntries);
 
           expect(cacheKeys).toHaveLength(1);
@@ -237,10 +328,11 @@ describe('cache option', () => {
           cacheKeys.forEach((cacheKey) => {
             // eslint-disable-next-line no-new-func
             const cacheEntry = new Function(
-              `'use strict'\nreturn ${cacheKey}`
+              `'use strict'\nreturn (${cacheKey});`
             )();
 
-            expect(cacheEntry.pattern.from).toBe(from);
+            // expect(cacheEntry.pattern.from).toBe(from);
+            expect(cacheEntry.key).toBe('foobar');
           });
         })
       )
@@ -248,48 +340,147 @@ describe('cache option', () => {
       .catch(done);
   });
 
-  it('should cache when "from" is a file', (done) => {
+  it('should cache file with custom object cache keys and custom cache directory when "cacheTransform" is an object', (done) => {
+    const newContent = 'newchanged!';
     const from = 'file.txt';
 
     runEmit({
-      expectedAssetKeys: ['subdir/test.txt'],
+      expectedAssetKeys: ['file.txt'],
       expectedAssetContent: {
-        'subdir/test.txt': 'newchanged!',
+        'file.txt': newContent,
       },
       patterns: [
         {
           from,
-          cacheTransform: true,
-          transform: function transform(content) {
+          cacheTransform: {
+            directory: cacheDir3,
+            keys: {
+              key: 'foobar',
+            },
+          },
+          transform(content) {
             return new Promise((resolve) => {
               resolve(`${content}changed!`);
             });
           },
-          transformPath(targetPath, absoluteFrom) {
-            expect(absoluteFrom).toBe(path.join(FIXTURES_DIR, 'file.txt'));
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(cacheDir3).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+
+          cacheKeys.forEach((cacheKey) => {
+            expect(cacheKey).toContain('foobar');
+          });
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
 
-            return targetPath.replace('file.txt', 'subdir/test.txt');
+  it('should cache file with custom function cache keys and custom cache directory when "cacheTransform" is an object', (done) => {
+    const newContent = 'newchanged!';
+    const from = 'file.txt';
+
+    runEmit({
+      expectedAssetKeys: ['file.txt'],
+      expectedAssetContent: {
+        'file.txt': newContent,
+      },
+      patterns: [
+        {
+          from,
+          cacheTransform: {
+            directory: cacheDir4,
+            keys: (defaultCacheKeys, absoluteFrom) => {
+              expect(absoluteFrom).toBeDefined();
+
+              return {
+                ...defaultCacheKeys,
+                key: 'foobar',
+              };
+            },
+          },
+          transform(content) {
+            return new Promise((resolve) => {
+              resolve(`${content}changed!`);
+            });
           },
         },
       ],
     })
       .then(() =>
-        cacache.ls(cacheDir).then((cacheEntries) => {
+        cacache.ls(cacheDir4).then((cacheEntries) => {
           const cacheKeys = Object.keys(cacheEntries);
 
           expect(cacheKeys).toHaveLength(1);
 
           cacheKeys.forEach((cacheKey) => {
-            // eslint-disable-next-line no-new-func
-            const cacheEntry = new Function(
-              `'use strict'\nreturn ${cacheKey}`
-            )();
-
-            expect(cacheEntry.pattern.from).toBe(from);
+            expect(cacheKey).toContain('foobar');
           });
         })
       )
       .then(done)
       .catch(done);
   });
+
+  it('should cache binary file', (done) => {
+    const from = 'file.txt.gz';
+    const content = fs.readFileSync(path.join(FIXTURES_DIR, from));
+    const expectedNewContent = zlib.gzipSync('newchanged!');
+
+    expect(isGzip(content)).toBe(true);
+    expect(isGzip(expectedNewContent)).toBe(true);
+
+    runEmit({
+      expectedAssetKeys: ['file.txt.gz'],
+      expectedAssetContent: {
+        'file.txt.gz': expectedNewContent,
+      },
+      patterns: [
+        {
+          from,
+          cacheTransform: true,
+          // eslint-disable-next-line no-shadow
+          transform: function transform(content) {
+            expect(isGzip(content)).toBe(true);
+
+            return new Promise((resolve) => {
+              // eslint-disable-next-line no-shadow
+              zlib.unzip(content, (error, content) => {
+                if (error) {
+                  throw error;
+                }
+
+                const newContent = Buffer.from(`${content}changed!`);
+
+                // eslint-disable-next-line no-shadow
+                zlib.gzip(newContent, (error, compressedData) => {
+                  if (error) {
+                    throw error;
+                  }
+
+                  expect(isGzip(compressedData)).toBe(true);
+
+                  return resolve(compressedData);
+                });
+              });
+            });
+          },
+        },
+      ],
+    })
+      .then(() =>
+        cacache.ls(defaultCacheDir).then((cacheEntries) => {
+          const cacheKeys = Object.keys(cacheEntries);
+
+          expect(cacheKeys).toHaveLength(1);
+        })
+      )
+      .then(done)
+      .catch(done);
+  });
 });
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index 1f9ddcb..0883af1 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -1,20 +1,8 @@
-import CopyPlugin from '../src/index';
-
-// Todo remove after dorp node@6 support
-if (!Object.entries) {
-  Object.entries = function entries(obj) {
-    const ownProps = Object.keys(obj);
-    let i = ownProps.length;
-    const resArray = new Array(i);
+import os from 'os';
 
-    // eslint-disable-next-line no-plusplus
-    while (i--) {
-      resArray[i] = [ownProps[i], obj[ownProps[i]]];
-    }
+import findCacheDir from 'find-cache-dir';
 
-    return resArray;
-  };
-}
+import CopyPlugin from '../src/index';
 
 describe('validate options', () => {
   const tests = {
@@ -71,13 +59,76 @@ describe('validate options', () => {
             },
           },
         ],
+        [
+          {
+            from: 'test.txt',
+            to: 'dir',
+            context: 'context',
+            cacheTransform:
+              findCacheDir({ name: 'copy-webpack-plugin-a' }) || os.tmpdir(),
+          },
+        ],
         [
           {
             from: 'test.txt',
             to: 'dir',
             context: 'context',
             cacheTransform: {
-              foo: 'bar',
+              keys: {
+                foo: 'bar',
+              },
+            },
+          },
+        ],
+        [
+          {
+            from: 'test.txt',
+            to: 'dir',
+            context: 'context',
+            cacheTransform: {
+              keys: () => ({
+                foo: 'bar',
+              }),
+            },
+          },
+        ],
+        [
+          {
+            from: 'test.txt',
+            to: 'dir',
+            context: 'context',
+            cacheTransform: {
+              keys: async () => ({
+                foo: 'bar',
+              }),
+            },
+          },
+        ],
+        [
+          {
+            from: 'test.txt',
+            to: 'dir',
+            context: 'context',
+            cacheTransform: {
+              directory:
+                findCacheDir({ name: 'copy-webpack-plugin-b' }) || os.tmpdir(),
+              keys: {
+                foo: 'bar',
+              },
+            },
+          },
+        ],
+        [
+          {
+            from: 'test.txt',
+            to: 'dir',
+            context: 'context',
+            cacheTransform: {
+              directory:
+                findCacheDir({ name: 'copy-webpack-plugin-c' }) || os.tmpdir(),
+              keys: () => ({
+                foo: 'bar',
+              }),
             },
           },
         ],
@@ -150,6 +201,16 @@ describe('validate options', () => {
             cacheTransform: () => {},
           },
         ],
+        [
+          {
+            from: 'test.txt',
+            to: 'dir',
+            context: 'context',
+            cacheTransform: {
+              foo: 'bar',
+            },
+          },
+        ],
         [
           {
             from: 'test.txt',