Repository: webpack-contrib/copy-webpack-plugin
Pull request #463 (2020-05-10 13:40:05)

feat: remove ignore option in favor globOptions.ignore
<!--
  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**
- [x] new **feature**
- [ ] **code refactor**
- [x] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [x] **metadata update**

### Motivation / Use-Case

remove ignore option in favor globOptions.ignore
#449 

### Breaking Changes

Yes.

### Additional Info

Drop minimatch syntax for `ignore` in favor [fast-glob syntax](https://github.com/mrmlnc/fast-glob#pattern-syntax)



codecov[bot]: # [Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463?src=pr&el=h1) Report
> Merging [#463](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463?src=pr&el=desc) into [master](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/commit/6b07a63d27da45c84c047d3f4b4b22c758aeeb6f&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463/graphs/tree.svg?width=650&height=150&src=pr&token=fEoU9iBFGL)](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #463      +/-   ##
==========================================
- Coverage   94.56%   94.54%   -0.02%     
==========================================
  Files           9        8       -1     
  Lines         239      220      -19     
  Branches       61       56       -5     
==========================================
- Hits          226      208      -18     
+ Misses         12       11       -1     
  Partials        1        1              
```


| [Impacted Files](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/index.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463/diff?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `100.00% <ø> (ø)` | |
| [src/preProcessPattern.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463/diff?src=pr&el=tree#diff-c3JjL3ByZVByb2Nlc3NQYXR0ZXJuLmpz) | `93.02% <ø> (-0.16%)` | :arrow_down: |
| [src/processPattern.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463/diff?src=pr&el=tree#diff-c3JjL3Byb2Nlc3NQYXR0ZXJuLmpz) | `96.87% <ø> (+0.95%)` | :arrow_up: |
| [src/utils/createPatternGlob.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463/diff?src=pr&el=tree#diff-c3JjL3V0aWxzL2NyZWF0ZVBhdHRlcm5HbG9iLmpz) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463?src=pr&el=footer). Last update [6b07a63...f8a5932](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/463?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 9effdf5..410ee9a 100644
--- a/README.md
+++ b/README.md
@@ -79,11 +79,10 @@ module.exports = {
 |           [`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                                   |
+|    [`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.                                        |
 |           [`test`](#test)           | `{String\|RegExp}`  |                   `undefined`                   | Pattern for extracting elements to be used in `to` templates.                                         |
 |          [`force`](#force)          |     `{Boolean}`     |                     `false`                     | Overwrites files already in `compilation.assets` (usually added by other plugins/loaders).            |
-|         [`ignore`](#ignore)         |      `{Array}`      |                      `[]`                       | Globs to ignore files.                                                                                |
 |        [`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. |
@@ -95,7 +94,8 @@ Type: `String`
 Default: `undefined`
 
 Glob or path from where we сopy files.
-Globs accept [micromatch options](https://github.com/micromatch/micromatch).
+Globs accept [fast-glob pattern-syntax](https://github.com/mrmlnc/fast-glob#pattern-syntax).
+Glob can only be a `string`.
 
 > ⚠️ Don't use directly `\\` in `from` (i.e `path\to\file.ext`) option because on UNIX the backslash is a valid character inside a path component, i.e., it's not a separator.
 > On Windows, the forward slash and the backward slash are both separators.
@@ -126,21 +126,62 @@ module.exports = {
 
 If you define `from` as file path or folder path on `Windows`, you can use windows path segment (`\\`)
 
-```
-...
-from: path.resolve('__dirname', 'file.txt'),
-...
+```js
+module.exports = {
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        {
+          from: path.resolve('__dirname', 'file.txt'),
+        },
+      ],
+    }),
+  ],
+};
 ```
 
 But you should always use forward-slashes in `glob` expressions
 See [fast-glob manual](https://github.com/mrmlnc/fast-glob#how-to-write-patterns-on-windows).
 
+```js
+const FIXTURES_DIR_NORMALIZED = path
+  .resolve(__dirname, 'fixtures')
+  .replace(/\\/g, '/');
+
+module.exports = {
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        {
+          from: path.posix.join(FIXTURES_DIR_NORMALIZED, 'file.txt'),
+        },
+      ],
+    }),
+  ],
+};
 ```
-...
-const FIXTURES_DIR_NORMALIZED = path.resolve(__dirname, 'fixtures').replace(/\\/g, '/');
 
-from: path.posix.join(FIXTURES_DIR_NORMALIZED, 'file.txt'),
-...
+##### `For exclude files`
+
+To exclude files from the selection, you should use [globOptions.ignore option](https://github.com/mrmlnc/fast-glob#ignore)
+
+**webpack.config.js**
+
+```js
+module.exports = {
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        {
+          from: '**/*',
+          globOptions: {
+            ignore: ['**/file.*', '**/ignored-directory/**'],
+          },
+        },
+      ],
+    }),
+  ],
+};
 ```
 
 #### `to`
@@ -367,51 +408,6 @@ module.exports = {
 };
 ```
 
-#### `ignore`
-
-Type: `Array`
-Default: `[]`
-
-Globs to ignore files.
-
-**webpack.config.js**
-
-```js
-module.exports = {
-  plugins: [
-    new CopyPlugin({
-      patterns: [
-        {
-          from: 'src/**/*',
-          to: 'dest/',
-          ignore: ['*.js'],
-        },
-      ],
-    }),
-  ],
-};
-```
-
-> ⚠️ Note that only relative path should be provided to ignore option, an example to ignore `src/assets/subfolder/ignorefile.js` :
-
-**webpack.config.js**
-
-```js
-module.exports = {
-  plugins: [
-    new CopyPlugin({
-      patterns: [
-        {
-          from: 'src/assets',
-          to: 'dest/',
-          ignore: ['subfolder/ignorefile.js'],
-        },
-      ],
-    }),
-  ],
-};
-```
-
 #### `flatten`
 
 Type: `Boolean`
@@ -566,12 +562,6 @@ module.exports = {
 };
 ```
 
-### Options
-
-|        Name         |   Type    | Default | Description                                  |
-| :-----------------: | :-------: | :-----: | :------------------------------------------- |
-| [`ignore`](#ignore) | `{Array}` |  `[]`   | Array of globs to ignore (applied to `from`) |
-
 #### `ignore`
 
 Array of globs to ignore (applied to `from`).
diff --git a/package.json b/package.json
index e7a78d2..b7c1a46 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,6 @@
     "glob-parent": "^5.1.1",
     "globby": "^11.0.0",
     "loader-utils": "^2.0.0",
-    "minimatch": "^3.0.4",
     "normalize-path": "^3.0.0",
     "p-limit": "^2.3.0",
     "schema-utils": "^2.6.6",
diff --git a/src/index.js b/src/index.js
index 7c6a868..1dadcf2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -33,7 +33,6 @@ class CopyPlugin {
             compilation,
             inputFileSystem: compiler.inputFileSystem,
             output: compiler.options.output.path,
-            ignore: this.options.ignore || [],
             concurrency: this.options.concurrency,
           };
 
diff --git a/src/options.json b/src/options.json
index 9b5f6ea..8eb8d99 100644
--- a/src/options.json
+++ b/src/options.json
@@ -29,19 +29,6 @@
         "force": {
           "type": "boolean"
         },
-        "ignore": {
-          "type": "array",
-          "items": {
-            "anyOf": [
-              {
-                "type": "string"
-              },
-              {
-                "type": "object"
-              }
-            ]
-          }
-        },
         "flatten": {
           "type": "boolean"
         },
@@ -89,11 +76,7 @@
     "options": {
       "type": "object",
       "additionalProperties": false,
-      "properties": {
-        "ignore": {
-          "type": "array"
-        }
-      }
+      "properties": {}
     }
   }
 }
diff --git a/src/preProcessPattern.js b/src/preProcessPattern.js
index 5561eee..7748bae 100644
--- a/src/preProcessPattern.js
+++ b/src/preProcessPattern.js
@@ -29,8 +29,6 @@ export default async function preProcessPattern(globalRef, pattern) {
   pattern.context = path.normalize(pattern.context);
   pattern.to = path.normalize(pattern.to);
 
-  pattern.ignore = globalRef.ignore.concat(pattern.ignore || []);
-
   logger.debug(`processing from: '${pattern.from}' to: '${pattern.to}'`);
 
   switch (true) {
diff --git a/src/processPattern.js b/src/processPattern.js
index bcab016..3359bc1 100644
--- a/src/processPattern.js
+++ b/src/processPattern.js
@@ -2,9 +2,7 @@ import path from 'path';
 
 import globby from 'globby';
 import pLimit from 'p-limit';
-import minimatch from 'minimatch';
 
-import isObject from './utils/isObject';
 import createPatternGlob from './utils/createPatternGlob';
 
 /* eslint-disable no-param-reassign */
@@ -56,47 +54,6 @@ export default async function processPattern(globalRef, pattern) {
 
         logger.debug(`found ${from}`);
 
-        // Check the ignore list
-        let il = pattern.ignore.length;
-
-        // eslint-disable-next-line no-plusplus
-        while (il--) {
-          const ignoreGlob = pattern.ignore[il];
-
-          let globParams = {
-            dot: true,
-            matchBase: true,
-          };
-
-          let glob;
-
-          if (typeof ignoreGlob === 'string') {
-            glob = ignoreGlob;
-          } else if (isObject(ignoreGlob)) {
-            glob = ignoreGlob.glob || '';
-
-            const ignoreGlobParams = Object.assign({}, ignoreGlob);
-            delete ignoreGlobParams.glob;
-
-            // Overwrite minimatch defaults
-            globParams = Object.assign(globParams, ignoreGlobParams);
-          } else {
-            glob = '';
-          }
-
-          logger.debug(`testing ${glob} against ${file.relativeFrom}`);
-
-          if (minimatch(file.relativeFrom, glob, globParams)) {
-            logger.log(
-              `ignoring '${file.relativeFrom}', because it matches the ignore glob '${glob}'`
-            );
-
-            return Promise.resolve();
-          }
-
-          logger.debug(`${glob} doesn't match ${file.relativeFrom}`);
-        }
-
         // Change the to path to be relative for webpack
         if (pattern.toType === 'dir') {
           file.webpackTo = path.join(pattern.to, file.relativeFrom);
diff --git a/src/utils/createPatternGlob.js b/src/utils/createPatternGlob.js
index 7403569..b35bb43 100644
--- a/src/utils/createPatternGlob.js
+++ b/src/utils/createPatternGlob.js
@@ -38,9 +38,11 @@ function createPatternGlob(pattern, globalRef) {
         '**/*'
       );
       pattern.absoluteFrom = path.join(pattern.absoluteFrom, '**/*');
-      pattern.globOptions = {
-        dot: true,
-      };
+
+      if (typeof pattern.globOptions.dot === 'undefined') {
+        pattern.globOptions.dot = true;
+      }
+
       break;
 
     case 'file':
@@ -50,9 +52,11 @@ function createPatternGlob(pattern, globalRef) {
 
       pattern.context = path.dirname(pattern.absoluteFrom);
       pattern.glob = getAbsoluteContext(pattern.absoluteFrom);
-      pattern.globOptions = {
-        dot: true,
-      };
+
+      if (typeof pattern.globOptions.dot === 'undefined') {
+        pattern.globOptions.dot = true;
+      }
+
       break;
 
     default:
@@ -67,7 +71,6 @@ function createPatternGlob(pattern, globalRef) {
       compilation.contextDependencies.add(contextDependencies);
 
       pattern.fromType = 'glob';
-      pattern.globOptions = pattern.globOptions || {};
       pattern.glob = path.isAbsolute(pattern.fromOrigin)
         ? pattern.fromOrigin
         : path.posix.join(
diff --git a/src/utils/isObject.js b/src/utils/isObject.js
deleted file mode 100644
index 2f60ad8..0000000
--- a/src/utils/isObject.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export default (val) =>
-  Object.prototype.toString.call(val) === '[object Object]';


Test patch:
diff --git a/test/__snapshots__/CopyPlugin.test.js.snap b/test/__snapshots__/CopyPlugin.test.js.snap
index 71946eb..c9a8de6 100644
--- a/test/__snapshots__/CopyPlugin.test.js.snap
+++ b/test/__snapshots__/CopyPlugin.test.js.snap
@@ -10,14 +10,6 @@ Object {
     "add ./fixtures/file.txt as fileDependencies",
     "begin globbing './fixtures/file.txt' with a context of './fixtures'",
     "found ./fixtures/file.txt",
-    "testing watch/**/* against file.txt",
-    "watch/**/* doesn't match file.txt",
-    "testing directory-ln against file.txt",
-    "directory-ln doesn't match file.txt",
-    "testing file-ln.txt against file.txt",
-    "file-ln.txt doesn't match file.txt",
-    "testing symlink/**/* against file.txt",
-    "symlink/**/* doesn't match file.txt",
     "determined that './fixtures/file.txt' should write to 'file.txt'",
     "getting stats for './fixtures/file.txt' to write to assets",
     "reading './fixtures/file.txt' to write to assets",
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index 336e5ad..4bfa80b 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -1,21 +1,15 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`validate options should throw an error on the "options" option with "{"ignore":true}" value 1`] = `
-"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
- - options.options.ignore should be an array:
-   [any, ...]"
-`;
-
 exports[`validate options should throw an error on the "options" option with "{"unknown":true}" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
  - options.options has an unknown property 'unknown'. These properties are valid:
-   object { ignore? }"
+   object {}"
 `;
 
 exports[`validate options should throw an error on the "patterns" option with "" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
  - options.patterns should be an array:
-   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "patterns" option with "[""]" value 1`] = `
@@ -43,42 +37,10 @@ exports[`validate options should throw an error on the "patterns" option with "[
  - options.patterns[0].force should be a boolean."
 `;
 
-exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","ignore":["test.txt",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] should be one of these:
-   non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }
-   Details:
-    * options.patterns[0].ignore[1] should be one of these:
-      string | object { … }
-      Details:
-       * options.patterns[0].ignore[1] should be a string.
-       * options.patterns[0].ignore[1] should be an object:
-         object { … }"
-`;
-
-exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","ignore":[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] should be one of these:
-   non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }
-   Details:
-    * options.patterns[0].ignore[0] should be one of these:
-      string | object { … }
-      Details:
-       * options.patterns[0].ignore[0] should be a string.
-       * options.patterns[0].ignore[0] should be an object:
-         object { … }"
-`;
-
-exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","ignore":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].ignore should be an array:
-   [string | object { … }, ...]"
-`;
-
 exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","test":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] should be one of these:
-   non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }
+   non-empty string | object { from, to?, context?, toType?, test?, force?, flatten?, transform?, cacheTransform?, transformPath?, … }
    Details:
     * options.patterns[0].test should be one of these:
       string | RegExp
@@ -106,7 +68,7 @@ exports[`validate options should throw an error on the "patterns" option with "[
 exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context"}]" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
  - options.patterns[0] should be one of these:
-   non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }
+   non-empty string | object { from, to?, context?, toType?, test?, force?, flatten?, transform?, cacheTransform?, transformPath?, … }
    Details:
     * options.patterns[0].cacheTransform should be one of these:
       boolean | object { … }
@@ -145,19 +107,19 @@ exports[`validate options should throw an error on the "patterns" option with "[
 exports[`validate options should throw an error on the "patterns" option with "{}" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
  - options.patterns should be an array:
-   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "patterns" option with "true" value 1`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
  - options.patterns should be an array:
-   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "patterns" option with "true" value 2`] = `
 "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
  - options.patterns should be an array:
-   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, flatten?, transform?, cacheTransform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = `
diff --git a/test/from-option.test.js b/test/from-option.test.js
index 5ea0ace..46740f6 100644
--- a/test/from-option.test.js
+++ b/test/from-option.test.js
@@ -67,6 +67,14 @@ describe('from option', () => {
     it('should move a file (symbolic link)', (done) => {
       runEmit({
         symlink: true,
+        expectedWarnings:
+          process.platform === 'win32'
+            ? [
+                new Error(
+                  `unable to locate 'symlink${path.sep}file-ln.txt' at '${FIXTURES_DIR}${path.sep}symlink${path.sep}file-ln.txt'`
+                ),
+              ]
+            : [],
         expectedAssetKeys: process.platform === 'win32' ? [] : ['file-ln.txt'],
         patterns: [
           {
@@ -198,6 +206,14 @@ describe('from option', () => {
       runEmit({
         // Windows doesn't support symbolic link
         symlink: true,
+        expectedWarnings:
+          process.platform === 'win32'
+            ? [
+                new Error(
+                  `unable to locate 'symlink${path.sep}directory-ln' at '${FIXTURES_DIR}${path.sep}symlink${path.sep}directory-ln'`
+                ),
+              ]
+            : [],
         expectedAssetKeys:
           process.platform === 'win32'
             ? []
@@ -455,6 +471,14 @@ describe('from option', () => {
       runEmit({
         // Windows doesn't support symbolic link
         symlink: true,
+        expectedWarnings:
+          process.platform === 'win32'
+            ? [
+                new Error(
+                  `unable to locate 'symlink\\**\\*.txt' at '${FIXTURES_DIR}${path.sep}symlink\\**\\*.txt'`
+                ),
+              ]
+            : [],
         expectedAssetKeys:
           process.platform === 'win32'
             ? []
diff --git a/test/globOptions-option.test.js b/test/globOptions-option.test.js
index 2198b26..020163b 100644
--- a/test/globOptions-option.test.js
+++ b/test/globOptions-option.test.js
@@ -1,5 +1,9 @@
+import path from 'path';
+
 import { runEmit } from './helpers/run';
 
+const FIXTURES_DIR = path.join(__dirname, 'fixtures');
+
 describe('from option', () => {
   it('should move files exclude dot files', (done) => {
     runEmit({
@@ -33,3 +37,223 @@ describe('from option', () => {
       .catch(done);
   });
 });
+
+describe('globOptions ignore option', () => {
+  it('should ignore files when "from" is a file', (done) => {
+    runEmit({
+      expectedWarnings: [
+        new Error(
+          `unable to locate 'file.txt' at '${FIXTURES_DIR}${path.sep}file.txt'`
+        ),
+      ],
+      patterns: [
+        {
+          from: 'file.txt',
+          globOptions: {
+            ignore: ['**/file.*'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        '.dottedfile',
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            ignore: ['**/nestedfile.*'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files in nested directory when "from" is a directory', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.dottedfile', 'directoryfile.txt'],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            ignore: ['**/nested/**'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files when from is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directory/directoryfile.txt',
+        'directory/nested/deep-nested/deepnested.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory/**/*',
+          globOptions: {
+            ignore: ['**/nestedfile.*'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should files in nested directory when from is a glob', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directory/directoryfile.txt'],
+      patterns: [
+        {
+          from: 'directory/**/*',
+          globOptions: {
+            ignore: ['**/nested/**'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files with a certain extension', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.dottedfile'],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            ignore: ['**/*.txt'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files with multiple ignore patterns', (done) => {
+    runEmit({
+      expectedAssetKeys: ['directory/nested/nestedfile.txt'],
+      patterns: [
+        {
+          from: 'directory/**/*',
+          globOptions: {
+            ignore: ['**/directoryfile.*', '**/deep-nested/**'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files with flatten true', (done) => {
+    runEmit({
+      expectedAssetKeys: ['img/.dottedfile', 'img/nestedfile.txt'],
+      patterns: [
+        {
+          from: 'directory/',
+          to: 'img/',
+          flatten: true,
+          globOptions: {
+            ignore: ['**/directoryfile.*', '**/deep-nested/**'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files except those with dots', (done) => {
+    runEmit({
+      expectedAssetKeys: ['.dottedfile'],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            ignore: ['!(**/.*)'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files that start with a dot', (done) => {
+    runEmit({
+      expectedAssetKeys: [
+        'directoryfile.txt',
+        'nested/deep-nested/deepnested.txt',
+        'nested/nestedfile.txt',
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            ignore: ['**/.*'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignores all files even if they start with a dot', (done) => {
+    runEmit({
+      expectedWarnings: [
+        new Error(
+          `unable to locate 'directory' at '${FIXTURES_DIR}${path.sep}directory${path.sep}**${path.sep}*'`
+        ),
+      ],
+      patterns: [
+        {
+          from: 'directory',
+          globOptions: {
+            ignore: ['**/*'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+
+  it('should ignore files when "from" is a file (global ignore)', (done) => {
+    runEmit({
+      expectedWarnings: [
+        new Error(
+          `unable to locate 'file.txt' at '${FIXTURES_DIR}${path.sep}file.txt'`
+        ),
+      ],
+      patterns: [
+        {
+          ignore: ['file.*'],
+          from: 'file.txt',
+          globOptions: {
+            ignore: ['**/file.*'],
+          },
+        },
+      ],
+    })
+      .then(done)
+      .catch(done);
+  });
+});
diff --git a/test/helpers/run.js b/test/helpers/run.js
index 66d5987..0cb41ca 100644
--- a/test/helpers/run.js
+++ b/test/helpers/run.js
@@ -10,6 +10,17 @@ import removeIllegalCharacterForWindows from './removeIllegalCharacterForWindows
 
 import { compile, getCompiler, readAssets } from './';
 
+/* eslint-disable no-param-reassign */
+
+const isWin = process.platform === 'win32';
+
+const ignore = [
+  '**/symlink/**/*',
+  '**/file-ln.txt',
+  '**/directory-ln',
+  '**/watch/**/*',
+];
+
 function run(opts) {
   return new Promise((resolve, reject) => {
     if (Array.isArray(opts.patterns)) {
@@ -18,32 +29,21 @@ function run(opts) {
           // eslint-disable-next-line no-param-reassign
           pattern.context = removeIllegalCharacterForWindows(pattern.context);
         }
+
+        if (typeof pattern !== 'string') {
+          if (!opts.symlink || isWin) {
+            pattern.globOptions = pattern.globOptions || {};
+            pattern.globOptions.ignore = [
+              ...ignore,
+              ...(pattern.globOptions.ignore || []),
+            ];
+          }
+        }
       });
     }
 
     const compiler = opts.compiler || getCompiler();
 
-    const isWin = process.platform === 'win32';
-
-    if (!opts.symlink || isWin) {
-      if (!opts.options) {
-        // eslint-disable-next-line no-param-reassign
-        opts.options = {};
-      }
-
-      if (!opts.options.ignore) {
-        // eslint-disable-next-line no-param-reassign
-        opts.options.ignore = [];
-      }
-
-      opts.options.ignore.push(
-        'symlink/**/*',
-        'file-ln.txt',
-        'directory-ln',
-        'watch/**/*'
-      );
-    }
-
     new CopyPlugin({ patterns: opts.patterns, options: opts.options }).apply(
       compiler
     );
diff --git a/test/ignore-option.test.js b/test/ignore-option.test.js
deleted file mode 100644
index f65d2cb..0000000
--- a/test/ignore-option.test.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import { runEmit } from './helpers/run';
-
-describe('ignore option', () => {
-  it('should ignore files when "from" is a file', (done) => {
-    runEmit({
-      expectedAssetKeys: [],
-      patterns: [
-        {
-          ignore: ['file.*'],
-          from: 'file.txt',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should files when "from" is a directory', (done) => {
-    runEmit({
-      expectedAssetKeys: [
-        '.dottedfile',
-        'directoryfile.txt',
-        'nested/deep-nested/deepnested.txt',
-      ],
-      patterns: [
-        {
-          ignore: ['*/nestedfile.*'],
-          from: 'directory',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should files in nested directory when "from" is a directory', (done) => {
-    runEmit({
-      expectedAssetKeys: ['.dottedfile', 'directoryfile.txt'],
-      patterns: [
-        {
-          ignore: ['**/nested/**'],
-          from: 'directory',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should files when from is a glob', (done) => {
-    runEmit({
-      expectedAssetKeys: [
-        'directory/directoryfile.txt',
-        'directory/nested/deep-nested/deepnested.txt',
-      ],
-      patterns: [
-        {
-          ignore: ['*nestedfile.*'],
-          from: 'directory/**/*',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should files in nested directory when from is a glob', (done) => {
-    runEmit({
-      expectedAssetKeys: ['directory/directoryfile.txt'],
-      patterns: [
-        {
-          ignore: ['*/nested/**'],
-          from: 'directory/**/*',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should ignore files with a certain extension', (done) => {
-    runEmit({
-      expectedAssetKeys: ['.dottedfile'],
-      patterns: [
-        {
-          ignore: ['*.txt'],
-          from: 'directory',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should ignore files with multiple ignore patterns', (done) => {
-    runEmit({
-      expectedAssetKeys: ['directory/nested/nestedfile.txt'],
-      patterns: [
-        {
-          ignore: ['directoryfile.*', '**/deep-nested/**'],
-          from: 'directory/**/*',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should ignore files except those with dots', (done) => {
-    runEmit({
-      expectedAssetKeys: ['.dottedfile'],
-      options: {
-        ignore: [
-          {
-            dot: false,
-            glob: '**/*',
-          },
-        ],
-      },
-      patterns: [
-        {
-          from: 'directory',
-          ignore: [
-            {
-              dot: false,
-              glob: '**/*',
-            },
-          ],
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should ignore files that start with a dot', (done) => {
-    runEmit({
-      expectedAssetKeys: [
-        'directoryfile.txt',
-        'nested/deep-nested/deepnested.txt',
-        'nested/nestedfile.txt',
-      ],
-      patterns: [
-        {
-          ignore: ['.dottedfile'],
-          from: 'directory',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should ignores all files even if they start with a dot', (done) => {
-    runEmit({
-      expectedAssetKeys: [],
-      options: {
-        ignore: ['**/*'],
-      },
-      patterns: [
-        {
-          from: 'directory',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-
-  it('should ignore files when "from" is a file (global ignore)', (done) => {
-    runEmit({
-      expectedAssetKeys: [],
-      options: {
-        ignore: ['file.*'],
-      },
-      patterns: [
-        {
-          ignore: ['file.*'],
-          from: 'file.txt',
-        },
-      ],
-    })
-      .then(done)
-      .catch(done);
-  });
-});
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index cf779cc..bd87b48 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -55,14 +55,6 @@ describe('validate options', () => {
             toType: 'file',
             test: /test/,
             force: true,
-            ignore: [
-              'ignore-1',
-              'ignore-2',
-              {
-                dot: false,
-                glob: '**/*',
-              },
-            ],
             flatten: true,
             transform: () => {},
             cacheTransform: true,
@@ -166,30 +158,6 @@ describe('validate options', () => {
             force: 'true',
           },
         ],
-        [
-          {
-            from: 'test.txt',
-            to: 'dir',
-            context: 'context',
-            ignore: true,
-          },
-        ],
-        [
-          {
-            from: 'test.txt',
-            to: 'dir',
-            context: 'context',
-            ignore: [true],
-          },
-        ],
-        [
-          {
-            from: 'test.txt',
-            to: 'dir',
-            context: 'context',
-            ignore: ['test.txt', true],
-          },
-        ],
         [
           {
             from: 'test.txt',
@@ -235,8 +203,8 @@ describe('validate options', () => {
       ],
     },
     options: {
-      success: [{ ignore: ['test'] }],
-      failure: [{ unknown: true }, { ignore: true }],
+      success: [{}],
+      failure: [{ unknown: true }],
     },
     unknown: {
       success: [],