Repository: webpack-contrib/copy-webpack-plugin
Pull request #451 (2020-04-28 15:42:49)

refactor: plugin options
<!--
  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**
- [ ] new **feature**
- [x] **code refactor**
- [ ] **test update** <!-- if bug or feature is checked, this should be too -->
- [ ] **typo fix**
- [ ] **metadata update**

### Motivation / Use-Case

Unify plugin options

### Breaking Changes

Yes

BREAKING CHANGE: the plugin now accepts an object as options, you should change `new CopyPlugin(patterns, options)` to `new CopyPlugin( { patterns, options })`

### Additional Info

No

codecov[bot]: # [Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/451?src=pr&el=h1) Report
> Merging [#451](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/451?src=pr&el=desc) into [master](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/commit/383ff9d9e8279a4298b7aebb61a02d0cfea0531c&el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.

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

```diff
@@           Coverage Diff           @@
##           master     #451   +/-   ##
=======================================
  Coverage   94.59%   94.59%           
=======================================
  Files           9        9           
  Lines         259      259           
  Branches       63       64    +1     
=======================================
  Hits          245      245           
  Misses         13       13           
  Partials        1        1           
```


| [Impacted Files](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/451?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/index.js](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/451/diff?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `95.45% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/451?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/451?src=pr&el=footer). Last update [383ff9d...53641f0](https://codecov.io/gh/webpack-contrib/copy-webpack-plugin/pull/451?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 bb359e1..5f4f34e 100644
--- a/README.md
+++ b/README.md
@@ -34,10 +34,12 @@ const CopyPlugin = require('copy-webpack-plugin');
 
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      { from: 'source', to: 'dest' },
-      { from: 'other', to: 'public' },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        { from: 'source', to: 'dest' },
+        { from: 'other', to: 'public' },
+      ],
+    }),
   ],
 };
 ```
@@ -53,8 +55,20 @@ The plugin's signature:
 **webpack.config.js**
 
 ```js
+const CopyPlugin = require('copy-webpack-plugin');
+
 module.exports = {
-  plugins: [new CopyPlugin(patterns, options)],
+  plugins: [
+    new CopyPlugin({
+      patterns: [
+        { from: 'source', to: 'dest' },
+        { from: 'other', to: 'public' },
+      ],
+      options: {
+        ignore: ['*.bin'],
+      },
+    }),
+  ],
 };
 ```
 
@@ -92,16 +106,18 @@ Globs accept [minimatch options](https://github.com/isaacs/minimatch).
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      'relative/path/to/file.ext',
-      '/absolute/path/to/file.ext',
-      'relative/path/to/dir',
-      '/absolute/path/to/dir',
-      '**/*',
-      {
-        from: '**/*',
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        'relative/path/to/file.ext',
+        '/absolute/path/to/file.ext',
+        'relative/path/to/dir',
+        '/absolute/path/to/dir',
+        '**/*',
+        {
+          from: '**/*',
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -122,20 +138,22 @@ Output path.
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: '**/*',
-        to: 'relative/path/to/dest/',
-      },
-      {
-        from: '**/*',
-        to: '/absolute/path/to/dest/',
-      },
-      {
-        from: '**/*',
-        to: '[path][name].[contenthash].[ext]',
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: '**/*',
+          to: 'relative/path/to/dest/',
+        },
+        {
+          from: '**/*',
+          to: '/absolute/path/to/dest/',
+        },
+        {
+          from: '**/*',
+          to: '[path][name].[contenthash].[ext]',
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -156,13 +174,15 @@ A path that determines how to interpret the `from` path.
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/*.txt',
-        to: 'dest/',
-        context: 'app/',
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.txt',
+          to: 'dest/',
+          context: 'app/',
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -190,13 +210,15 @@ We try to automatically determine the `type` so you most likely do not need this
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'path/to/file.txt',
-        to: 'directory/with/extension.ext',
-        toType: 'dir',
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'path/to/file.txt',
+          to: 'directory/with/extension.ext',
+          toType: 'dir',
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -208,13 +230,15 @@ module.exports = {
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'path/to/file.txt',
-        to: 'file/without/extension',
-        toType: 'file',
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'path/to/file.txt',
+          to: 'file/without/extension',
+          toType: 'file',
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -226,13 +250,15 @@ module.exports = {
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/',
-        to: 'dest/[name].[hash].[ext]',
-        toType: 'template',
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/',
+          to: 'dest/[name].[hash].[ext]',
+          toType: 'template',
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -255,13 +281,15 @@ and so on...
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: '*/*',
-        to: '[1]-[2].[hash].[ext]',
-        test: /([^/]+)\/(.+)\.png$/,
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: '*/*',
+          to: '[1]-[2].[hash].[ext]',
+          test: /([^/]+)\/(.+)\.png$/,
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -278,13 +306,15 @@ Overwrites files already in `compilation.assets` (usually added by other plugins
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/**/*',
-        to: 'dest/',
-        force: true,
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/**/*',
+          to: 'dest/',
+          force: true,
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -301,13 +331,15 @@ Globs to ignore files.
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/**/*',
-        to: 'dest/',
-        ignore: ['*.js'],
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/**/*',
+          to: 'dest/',
+          ignore: ['*.js'],
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -319,13 +351,15 @@ module.exports = {
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/assets',
-        to: 'dest/',
-        ignore: ['subfolder/ignorefile.js'],
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/assets',
+          to: 'dest/',
+          ignore: ['subfolder/ignorefile.js'],
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -344,13 +378,15 @@ Removes all directory references and only copies file names.
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/**/*',
-        to: 'dest/',
-        flatten: true,
-      },
-    ]),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/**/*',
+          to: 'dest/',
+          flatten: true,
+        },
+      ],
+    }),
   ],
 };
 ```
@@ -368,16 +404,18 @@ Default path to cache directory: `node_modules/.cache/copy-webpack-plugin`.
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/*.png',
-        to: 'dest/',
-        transform(content, path) {
-          return optimize(content);
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transform(content, path) {
+            return optimize(content);
+          },
+          cache: true,
         },
-        cache: true,
-      },
-    ]),
+      ],
+    }),
   ],
 };
 ```
@@ -394,15 +432,17 @@ Allows to modify the file contents.
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/*.png',
-        to: 'dest/',
-        transform(content, path) {
-          return optimize(content);
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transform(content, path) {
+            return optimize(content);
+          },
         },
-      },
-    ]),
+      ],
+    }),
   ],
 };
 ```
@@ -412,15 +452,17 @@ module.exports = {
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/*.png',
-        to: 'dest/',
-        transform(content, path) {
-          return Promise.resolve(optimize(content));
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transform(content, path) {
+            return Promise.resolve(optimize(content));
+          },
         },
-      },
-    ]),
+      ],
+    }),
   ],
 };
 ```
@@ -441,15 +483,17 @@ Allows to modify the writing path.
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/*.png',
-        to: 'dest/',
-        transformPath(targetPath, absolutePath) {
-          return 'newPath';
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transformPath(targetPath, absolutePath) {
+            return 'newPath';
+          },
         },
-      },
-    ]),
+      ],
+    }),
   ],
 };
 ```
@@ -459,15 +503,17 @@ module.exports = {
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'src/*.png',
-        to: 'dest/',
-        transformPath(targetPath, absolutePath) {
-          return Promise.resolve('newPath');
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'src/*.png',
+          to: 'dest/',
+          transformPath(targetPath, absolutePath) {
+            return Promise.resolve('newPath');
+          },
         },
-      },
-    ]),
+      ],
+    }),
   ],
 };
 ```
@@ -484,15 +530,17 @@ Allows to configute the glob pattern matching library used by the plugin. [See t
 ```js
 module.exports = {
   plugins: [
-    new CopyPlugin([
-      {
-        from: 'public/**/*',
-        globOptions: {
-          dot: true,
-          gitignore: true,
+    new CopyPlugin({
+      patterns: [
+        {
+          from: 'public/**/*',
+          globOptions: {
+            dot: true,
+            gitignore: true,
+          },
         },
-      },
-    ]),
+      ],
+    }),
   ],
 };
 ```
@@ -512,7 +560,12 @@ Array of globs to ignore (applied to `from`).
 
 ```js
 module.exports = {
-  plugins: [new CopyPlugin([...patterns], { ignore: ['*.js', '*.css'] })],
+  plugins: [
+    new CopyPlugin({
+      patterns: [...patterns],
+      options: { ignore: ['*.js', '*.css'] },
+    }),
+  ],
 };
 ```
 
@@ -524,7 +577,12 @@ A path that determines how to interpret the `from` path, shared for all patterns
 
 ```js
 module.exports = {
-  plugins: [new CopyPlugin([...patterns], { context: '/app' })],
+  plugins: [
+    new CopyPlugin({
+      patterns: [...patterns],
+      options: { context: '/app' },
+    }),
+  ],
 };
 ```
 
diff --git a/src/index.js b/src/index.js
index 6c1396d..cc5f27f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,15 +8,14 @@ import processPattern from './processPattern';
 import postProcessPattern from './postProcessPattern';
 
 class CopyPlugin {
-  constructor(patterns = [], options = {}) {
-    validateOptions(
-      schema,
-      { patterns, options },
-      { name: 'Copy Plugin', baseDataPath: 'options' }
-    );
-
-    this.patterns = patterns;
-    this.options = options;
+  constructor(options = {}) {
+    validateOptions(schema, options, {
+      name: 'Copy Plugin',
+      baseDataPath: 'options',
+    });
+
+    this.patterns = options.patterns;
+    this.options = options.options || {};
   }
 
   apply(compiler) {
diff --git a/src/options.json b/src/options.json
index 9f1f293..d8b627a 100644
--- a/src/options.json
+++ b/src/options.json
@@ -74,6 +74,7 @@
   "properties": {
     "patterns": {
       "type": "array",
+      "minItems": 1,
       "items": {
         "anyOf": [
           {


Test patch:
diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap
index 673fbe0..7864499 100644
--- a/test/__snapshots__/validate-options.test.js.snap
+++ b/test/__snapshots__/validate-options.test.js.snap
@@ -1,9 +1,20 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`validate options should throw an error on the "options" option with "{"context":true}" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options.options.context should be a string."
+`;
+
+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 "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?, cache?, transform?, transformPath?, … }, ...]"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, cache?, transform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
 `;
 
 exports[`validate options should throw an error on the "patterns" option with "[""]" value 1`] = `
@@ -11,6 +22,11 @@ exports[`validate options should throw an error on the "patterns" option with "[
  - options.patterns[0] should be an non-empty string."
 `;
 
+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 non-empty array."
+`;
+
 exports[`validate options should throw an error on the "patterns" option with "[{"from":"","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].from should be an non-empty string."
@@ -128,17 +144,65 @@ 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?, cache?, transform?, transformPath?, … }, ...]"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, cache?, transform?, 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?, cache?, transform?, transformPath?, … }, ...]"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, cache?, transform?, 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?, cache?, transform?, transformPath?, … }, ...]"
+   [non-empty string | object { from, to?, context?, toType?, test?, force?, ignore?, flatten?, cache?, transform?, transformPath?, … }, ...] (should not have fewer than 1 item)"
+`;
+
+exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
+`;
+
+exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
+`;
+
+exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
+`;
+
+exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
+`;
+
+exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
+`;
+
+exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
+`;
+
+exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = `
+"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
+ - options has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
+`;
+
+exports[`validate options should throw an error on the "unknown" 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 has an unknown property 'unknown'. These properties are valid:
+   object { patterns?, options? }"
 `;
diff --git a/test/helpers/run.js b/test/helpers/run.js
index 26efbbf..c7edbbb 100644
--- a/test/helpers/run.js
+++ b/test/helpers/run.js
@@ -42,7 +42,9 @@ function run(opts) {
       );
     }
 
-    new CopyPlugin(opts.patterns, opts.options).apply(compiler);
+    new CopyPlugin({ patterns: opts.patterns, options: opts.options }).apply(
+      compiler
+    );
 
     // Call the registered function with a mock compilation and callback
     const compilation = Object.assign(
diff --git a/test/validate-options.test.js b/test/validate-options.test.js
index 6a86753..fde45c0 100644
--- a/test/validate-options.test.js
+++ b/test/validate-options.test.js
@@ -20,7 +20,6 @@ describe('validate options', () => {
   const tests = {
     patterns: {
       success: [
-        [],
         ['test.txt'],
         ['test.txt', 'test-other.txt'],
         [
@@ -112,6 +111,7 @@ describe('validate options', () => {
         'true',
         '',
         {},
+        [],
         [''],
         [{}],
         [
@@ -234,6 +234,14 @@ describe('validate options', () => {
         ],
       ],
     },
+    options: {
+      success: [{ context: 'context' }, { ignore: ['test'] }],
+      failure: [{ context: true }, { ignore: true }],
+    },
+    unknown: {
+      success: [],
+      failure: [1, true, false, 'test', /test/, [], {}, { foo: 'bar' }],
+    },
   };
 
   function stringifyValue(value) {
@@ -255,7 +263,7 @@ describe('validate options', () => {
 
       try {
         // eslint-disable-next-line no-new
-        new CopyPlugin(key === 'patterns' ? value : { [key]: value });
+        new CopyPlugin({ [key]: value });
       } catch (errorFromPlugin) {
         if (errorFromPlugin.name !== 'ValidationError') {
           throw errorFromPlugin;