diff --git a/README.md b/README.md index 1008ea4f..23d61673 100644 --- a/README.md +++ b/README.md @@ -60,23 +60,23 @@ module.exports = { ### Patterns -| Name | Type | Default | Description | -| :-------------------------------: | :-------------------: | :---------------------------------------------: | :---------------------------------------------------------------------------------------------------- | -| [`from`](#from) | `{String\|Object}` | `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. | -| [`toType`](#totype) | `{String}` | `undefined` | Determinate what is `to` option - directory, file or template. | -| [`test`](#test) | `{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. | -| [`cache`](#cache) | `{Boolean\|Object}` | `false` | Enable `transform` caching. You can use `{ cache: { key: 'my-cache-key' } }` to invalidate the cache. | -| [`transform`](#transform) | `{Function\|Promise}` | `undefined` | Allows to modify the file contents. | -| [`transformPath`](#transformpath) | `{Function\|Promise}` | `undefined` | Allows to modify the writing path. | +| Name | Type | Default | Description | +| :-------------------------------: | :-----------------: | :---------------------------------------------: | :---------------------------------------------------------------------------------------------------- | +| [`from`](#from) | `{String\|Object}` | `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. | +| [`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. | +| [`cache`](#cache) | `{Boolean\|Object}` | `false` | Enable `transform` caching. You can use `{ cache: { key: 'my-cache-key' } }` to invalidate the cache. | +| [`transform`](#transform) | `{Function}` | `undefined` | Allows to modify the file contents. | +| [`transformPath`](#transformpath) | `{Function}` | `undefined` | Allows to modify the writing path. | #### `from` -Type: `String\|Object` +Type: `String|Object` Default: `undefined` Glob or path from where we сopy files. @@ -99,7 +99,6 @@ module.exports = { 'relative/path/to/dir', '/absolute/path/to/dir', '**/*', - { glob: '**/*', dot: false }, { from: '**/*', globOptions: { @@ -240,7 +239,7 @@ module.exports = { #### `test` -Type: `RegExp` +Type: `string|RegExp` Default: `undefined` Pattern for extracting elements to be used in `to` templates. @@ -367,13 +366,11 @@ module.exports = { #### `transform` -Type: `Function|Promise` +Type: `Function` Default: `undefined` Allows to modify the file contents. -##### `{Function}` - **webpack.config.js** ```js @@ -392,8 +389,6 @@ module.exports = { }; ``` -##### `{Promise}` - **webpack.config.js** ```js @@ -414,7 +409,7 @@ module.exports = { #### `transformPath` -Type: `Function|Promise` +Type: `Function` Default: `undefined` Allows to modify the writing path. @@ -423,8 +418,6 @@ Allows to modify the writing path. > On Windows, the forward slash and the backward slash are both separators. > Instead please use `/` or `path` methods. -##### `{Function}` - **webpack.config.js** ```js @@ -443,8 +436,6 @@ module.exports = { }; ``` -##### `{Promise}` - **webpack.config.js** ```js diff --git a/src/index.js b/src/index.js index b48a68aa..f72b4804 100644 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,16 @@ import path from 'path'; +import validateOptions from 'schema-utils'; import log from 'webpack-log'; +import schema from './options.json'; import preProcessPattern from './preProcessPattern'; import processPattern from './processPattern'; import postProcessPattern from './postProcessPattern'; class CopyPlugin { constructor(patterns = [], options = {}) { - if (!Array.isArray(patterns)) { - throw new Error('[copy-webpack-plugin] patterns must be an array'); - } + validateOptions(schema, patterns, this.constructor.name); this.patterns = patterns; this.options = options; diff --git a/src/options.json b/src/options.json new file mode 100644 index 00000000..cc3109ac --- /dev/null +++ b/src/options.json @@ -0,0 +1,81 @@ +{ + "definitions": { + "ObjectPattern": { + "type": "object", + "properties": { + "from": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "object" + } + ] + }, + "to": { + "type": "string" + }, + "context": { + "type": "string" + }, + "toType": { + "enum": ["dir", "file", "template"] + }, + "test": { + "anyOf": [ + { + "type": "string" + }, + { + "instanceof": "RegExp" + } + ] + }, + "force": { + "type": "boolean" + }, + "ignore": { + "type": "array" + }, + "flatten": { + "type": "boolean" + }, + "cache": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object" + } + ] + }, + "transform": { + "instanceof": "Function" + }, + "transformPath": { + "instanceof": "Function" + } + }, + "required": ["from"] + }, + "StringPattern": { + "type": "string", + "minLength": 1 + } + }, + "type": "array", + "minItems": 1, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/StringPattern" + }, + { + "$ref": "#/definitions/ObjectPattern" + } + ] + } +} diff --git a/src/preProcessPattern.js b/src/preProcessPattern.js index c7319188..4d79f6f7 100644 --- a/src/preProcessPattern.js +++ b/src/preProcessPattern.js @@ -25,14 +25,6 @@ export default function preProcessPattern(globalRef, pattern) { ? { from: pattern } : Object.assign({}, pattern); - if (pattern.from === '') { - const message = 'path "from" cannot be empty string'; - - logger.error(message); - - compilation.errors.push(new Error(message)); - } - pattern.to = pattern.to || ''; pattern.context = pattern.context || context; diff --git a/test/CopyPlugin.test.js b/test/CopyPlugin.test.js index 69a98152..5f25b63e 100644 --- a/test/CopyPlugin.test.js +++ b/test/CopyPlugin.test.js @@ -1,7 +1,5 @@ import path from 'path'; -import CopyPlugin from '../src/index'; - import { MockCompiler } from './utils/mocks'; import { run, runEmit, runChange } from './utils/run'; @@ -341,77 +339,6 @@ describe('apply function', () => { }); }); - describe('errors', () => { - it('should not throw an error if no patterns are passed', (done) => { - runEmit({ - expectedAssetKeys: [], - patterns: undefined, // eslint-disable-line no-undefined - }) - .then(done) - .catch(done); - }); - - it('should throw an error if the patterns are an object', () => { - const createPluginWithObject = () => { - // eslint-disable-next-line no-new - new CopyPlugin({}); - }; - - expect(createPluginWithObject).toThrow(Error); - }); - - it('should throw an error if the patterns are null', () => { - const createPluginWithNull = () => { - // eslint-disable-next-line no-new - new CopyPlugin(null); - }; - - expect(createPluginWithNull).toThrow(Error); - }); - - it('should throw an error if the "from" path is an empty string', () => { - const createPluginWithNull = () => { - // eslint-disable-next-line no-new - new CopyPlugin({ - from: '', - }); - }; - - expect(createPluginWithNull).toThrow(Error); - }); - - it('should warn when pattern is empty', (done) => { - runEmit({ - expectedAssetKeys: [ - '.file.txt', - '[!]/hello.txt', - '[special?directory]/(special-*file).txt', - '[special?directory]/directoryfile.txt', - '[special?directory]/nested/nestedfile.txt', - 'binextension.bin', - 'dir (86)/file.txt', - 'dir (86)/nesteddir/deepnesteddir/deepnesteddir.txt', - 'dir (86)/nesteddir/nestedfile.txt', - 'directory/.dottedfile', - 'directory/directoryfile.txt', - 'directory/nested/deep-nested/deepnested.txt', - 'directory/nested/nestedfile.txt', - 'file.txt', - 'file.txt.gz', - 'noextension', - ], - expectedErrors: [new Error(`path "from" cannot be empty string`)], - patterns: [ - { - from: '', - }, - ], - }) - .then(done) - .catch(done); - }); - }); - describe('dev server', () => { it('should work with absolute to if outpath is defined with webpack-dev-server', (done) => { runEmit({ diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap new file mode 100644 index 00000000..b5f2747d --- /dev/null +++ b/test/__snapshots__/validate-options.test.js.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validate options should throw an error on the "patterns" option with "" value 1`] = ` +"CopyPlugin Invalid Options + +options should be array +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[""]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should NOT be shorter than 1 characters +options.0 should be object +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"","to":"dir","context":"context"}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.from should NOT be shorter than 1 characters +options.0.from should be object +options.0.from should match some schema in anyOf +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","flatten":"true"}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.flatten should be boolean +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","force":"true"}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.force should be boolean +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","ignore":true}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.ignore should be array +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","test":true}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.test should be string +options.0.test should pass \\"instanceof\\" keyword validation +options.0.test should match some schema in anyOf +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","toType":"foo"}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.toType should be equal to one of the allowed values +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","transform":true}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.transform should pass \\"instanceof\\" keyword validation +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context","transformPath":true}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.transformPath should pass \\"instanceof\\" keyword validation +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":"context"}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.cache should be boolean +options.0.cache should be object +options.0.cache should match some schema in anyOf +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","context":true}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.context should be string +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":true,"context":"context"}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.to should be string +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":true,"to":"dir","context":"context"}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0.from should be string +options.0.from should be object +options.0.from should match some schema in anyOf +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "[{}]" value 1`] = ` +"CopyPlugin Invalid Options + +options.0 should be string +options.0 should have required property 'from' +options.0 should match some schema in anyOf +" +`; + +exports[`validate options should throw an error on the "patterns" option with "{}" value 1`] = ` +"CopyPlugin Invalid Options + +options should be array +" +`; + +exports[`validate options should throw an error on the "patterns" option with "true" value 1`] = ` +"CopyPlugin Invalid Options + +options should be array +" +`; + +exports[`validate options should throw an error on the "patterns" option with "true" value 2`] = ` +"CopyPlugin Invalid Options + +options should be array +" +`; diff --git a/test/validate-options.test.js b/test/validate-options.test.js new file mode 100644 index 00000000..0afaaa0e --- /dev/null +++ b/test/validate-options.test.js @@ -0,0 +1,263 @@ +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); + + // eslint-disable-next-line no-plusplus + while (i--) { + resArray[i] = [ownProps[i], obj[ownProps[i]]]; + } + + return resArray; + }; +} + +describe('validate options', () => { + const tests = { + patterns: { + success: [ + ['test.txt'], + ['test.txt', 'test-other.txt'], + [ + 'test.txt', + { + from: 'test.txt', + to: 'dir', + context: 'context', + }, + ], + [ + { + from: 'test.txt', + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + }, + ], + [ + { + from: 'test.txt', + context: 'context', + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + toType: 'file', + test: /test/, + force: true, + ignore: ['ignore-1', 'ignore-2'], + flatten: true, + cache: true, + transform: () => {}, + transformPath: () => {}, + }, + ], + [ + { + from: { + glob: '**/*', + dot: false, + }, + to: 'dir', + context: 'context', + globOptions: { + dot: false, + }, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + globOptions: { + dot: false, + }, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + test: 'test', + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + test: /test/, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + cache: { + foo: 'bar', + }, + }, + ], + ], + failure: [ + true, + 'true', + '', + {}, + [''], + [{}], + [ + { + from: '', + to: 'dir', + context: 'context', + }, + ], + [ + { + from: true, + to: 'dir', + context: 'context', + }, + ], + [ + { + from: 'test.txt', + to: true, + context: 'context', + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: true, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + toType: 'foo', + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + test: true, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + force: 'true', + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + ignore: true, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + flatten: 'true', + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + cache: () => {}, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + transform: true, + }, + ], + [ + { + from: 'test.txt', + to: 'dir', + context: 'context', + transformPath: true, + }, + ], + ], + }, + }; + + function stringifyValue(value) { + if ( + Array.isArray(value) || + (value && typeof value === 'object' && value.constructor === Object) + ) { + return JSON.stringify(value); + } + + return value; + } + + async function createTestCase(key, value, type) { + it(`should ${ + type === 'success' ? 'successfully validate' : 'throw an error on' + } the "${key}" option with "${stringifyValue(value)}" value`, async () => { + let error; + + try { + // eslint-disable-next-line no-new + new CopyPlugin(key === 'patterns' ? value : { [key]: value }); + } catch (errorFromPlugin) { + if (errorFromPlugin.name !== 'ValidationError') { + throw errorFromPlugin; + } + + error = errorFromPlugin; + } finally { + if (type === 'success') { + expect(error).toBeUndefined(); + } else if (type === 'failure') { + expect(() => { + throw error; + }).toThrowErrorMatchingSnapshot(); + } + } + }); + } + + for (const [key, values] of Object.entries(tests)) { + for (const type of Object.keys(values)) { + for (const value of values[type]) { + createTestCase(key, value, type); + } + } + } +});