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

Issue:
[Bug] Not modified files are copied on watch mode with copyUnmodified set to true (default)
Webpack copies all files, regardless of modification when using watch option on every watch check

Version: 4.2.3
Sample configuration:

```javascript
new CopyWebpackPlugin([
      {
        context: "./src",
        from: "**/*.txt",
        to: "dest1"
      }, {
        context: "./src",
        from: "**/*.txt",
        to: "dest2"
      }
])
```

Pull request description:
fix: copy only modified files when you use patterns with difference to
<!--
  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:

- [x] **bugfix**
- [ ] new **feature**
- [ ] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

fixes #181

### Breaking Changes

No

### Additional Info

No

Patch:
diff --git a/src/postProcessPattern.js b/src/postProcessPattern.js
index 48f6b97..07d5c1a 100644
--- a/src/postProcessPattern.js
+++ b/src/postProcessPattern.js
@@ -6,6 +6,7 @@ import loaderUtils from 'loader-utils';
 import cacache from 'cacache';
 import serialize from 'serialize-javascript';
 import findCacheDir from 'find-cache-dir';
+import normalizePath from 'normalize-path';
 
 import { name, version } from '../package.json';
 
@@ -96,14 +97,17 @@ export default function postProcessPattern(globalRef, pattern, file) {
             file.webpackTo = file.webpackTo.replace(/\.?\[ext\]/g, '');
           }
 
-          file.webpackTo = loaderUtils.interpolateName(
-            { resourcePath: file.absoluteFrom },
-            file.webpackTo,
-            {
-              content,
-              regExp: file.webpackToRegExp,
-              context: pattern.context,
-            }
+          // Developers can use invalid slashes in regex we should fix it
+          file.webpackTo = normalizePath(
+            loaderUtils.interpolateName(
+              { resourcePath: file.absoluteFrom },
+              file.webpackTo,
+              {
+                content,
+                regExp: file.webpackToRegExp,
+                context: pattern.context,
+              }
+            )
           );
         }
 
@@ -115,7 +119,8 @@ export default function postProcessPattern(globalRef, pattern, file) {
             pattern.transformPath(file.webpackTo, file.absoluteFrom)
           )
             .then((newPath) => {
-              file.webpackTo = newPath;
+              // Developers can use invalid slashes we should fix it
+              file.webpackTo = normalizePath(newPath);
             })
             .then(() => content);
         }
@@ -127,9 +132,9 @@ export default function postProcessPattern(globalRef, pattern, file) {
 
         if (
           !copyUnmodified &&
-          written[file.absoluteFrom] &&
-          written[file.absoluteFrom].hash === hash &&
-          written[file.absoluteFrom].webpackTo === file.webpackTo
+          written[file.webpackTo] &&
+          written[file.webpackTo][file.absoluteFrom] &&
+          written[file.webpackTo][file.absoluteFrom] === hash
         ) {
           info(`skipping '${file.webpackTo}', because it hasn't changed`);
 
@@ -137,10 +142,12 @@ export default function postProcessPattern(globalRef, pattern, file) {
         }
 
         debug(`added ${hash} to written tracking for '${file.absoluteFrom}'`);
-        written[file.absoluteFrom] = {
-          hash,
-          webpackTo: file.webpackTo,
-        };
+
+        if (!written[file.webpackTo]) {
+          written[file.webpackTo] = {};
+        }
+
+        written[file.webpackTo][file.absoluteFrom] = hash;
 
         if (compilation.assets[file.webpackTo] && !file.force) {
           info(`skipping '${file.webpackTo}', because it already exists`);
diff --git a/src/preProcessPattern.js b/src/preProcessPattern.js
index e1c9c35..a319965 100644
--- a/src/preProcessPattern.js
+++ b/src/preProcessPattern.js
@@ -138,7 +138,9 @@ export default function preProcessPattern(globalRef, pattern) {
         pattern.fromType = 'dir';
         pattern.context = pattern.absoluteFrom;
         pattern.glob = normalize(pattern.absoluteFrom, '**/*');
-        pattern.absoluteFrom = path.join(pattern.absoluteFrom, '**/*');
+        pattern.absoluteFrom = normalizePath(
+          path.join(pattern.absoluteFrom, '**/*')
+        );
         pattern.globOptions = {
           dot: true,
         };
@@ -146,7 +148,7 @@ export default function preProcessPattern(globalRef, pattern) {
         fileDependencies.add(pattern.absoluteFrom);
 
         pattern.fromType = 'file';
-        pattern.context = path.dirname(pattern.absoluteFrom);
+        pattern.context = normalizePath(path.dirname(pattern.absoluteFrom));
         pattern.glob = normalize(pattern.absoluteFrom);
         pattern.globOptions = {
           dot: true,


Test patch:
diff --git a/test/CopyPlugin.test.js b/test/CopyPlugin.test.js
index 8844537..22a3130 100644
--- a/test/CopyPlugin.test.js
+++ b/test/CopyPlugin.test.js
@@ -1953,6 +1953,116 @@ describe('apply function', () => {
     });
   });
 
+  describe('modified files', () => {
+    it('copy only changed files', (done) => {
+      runChange({
+        expectedAssetKeys: ['dest1/tempfile1.txt'],
+        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
+        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
+        patterns: [
+          {
+            context: 'directory',
+            from: '**/*.txt',
+            to: 'dest1',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('copy only changed files (multiple patterns)', (done) => {
+      runChange({
+        expectedAssetKeys: ['dest1/tempfile1.txt', 'dest2/tempfile1.txt'],
+        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
+        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
+        patterns: [
+          {
+            context: 'directory',
+            from: '**/*.txt',
+            to: 'dest1',
+          },
+          {
+            context: 'directory',
+            from: '**/*.txt',
+            to: 'dest2',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('copy only changed files (multiple patterns with difference context)', (done) => {
+      runChange({
+        expectedAssetKeys: [
+          'dest1/tempfile1.txt',
+          'dest2/directory/tempfile1.txt',
+        ],
+        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
+        newFileLoc2: path.join(HELPER_DIR, 'tempfile2.txt'),
+        patterns: [
+          {
+            context: 'directory',
+            from: '**/*.txt',
+            to: 'dest1',
+          },
+          {
+            from: '**/*.txt',
+            to: 'dest2',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('copy only changed files (multiple patterns with difference context 1)', (done) => {
+      runChange({
+        expectedAssetKeys: [
+          'dest1/directory/tempfile1.txt',
+          'dest2/tempfile1.txt',
+        ],
+        newFileLoc1: path.join(HELPER_DIR, 'directory', 'tempfile1.txt'),
+        newFileLoc2: path.join(HELPER_DIR, 'tempfile2.txt'),
+        patterns: [
+          {
+            from: '**/*.txt',
+            to: 'dest1',
+          },
+          {
+            context: 'directory',
+            from: '**/*.txt',
+            to: 'dest2',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+
+    it('copy only changed files (multiple patterns with difference context 2)', (done) => {
+      runChange({
+        expectedAssetKeys: ['dest1/tempfile1.txt'],
+        newFileLoc1: path.join(HELPER_DIR, 'tempfile1.txt'),
+        newFileLoc2: path.join(HELPER_DIR, 'directory', 'tempfile2.txt'),
+        patterns: [
+          {
+            from: '**/*.txt',
+            to: 'dest1',
+          },
+          {
+            context: 'directory',
+            from: '**/*.txt',
+            to: 'dest2',
+          },
+        ],
+      })
+        .then(done)
+        .catch(done);
+    });
+  });
+
   describe('options', () => {
     describe('ignore', () => {
       it('ignores files when from is a file', (done) => {