From 1496f85d2fa5e87dccd0cda92b1343c649f3e5bd Mon Sep 17 00:00:00 2001 From: Evilebot Tnawi Date: Mon, 31 Aug 2020 18:10:18 +0300 Subject: [PATCH] feat: added `filter` option (#524) --- README.md | 36 +++++++++++ src/options.json | 3 + src/processPattern.js | 62 ++++++++++++------- .../validate-options.test.js.snap | 33 +++++----- test/filter-option.test.js | 56 +++++++++++++++++ test/validate-options.test.js | 12 ++++ 6 files changed, 164 insertions(+), 38 deletions(-) create mode 100644 test/filter-option.test.js diff --git a/README.md b/README.md index 8bf6b558..f3a46706 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ module.exports = { | [`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. | +| [`filter`](#filter) | `{Function}` | `undefined` | Allows to filter copied assets. | | [`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. | @@ -277,6 +278,41 @@ module.exports = { }; ``` +#### `filter` + +Type: `Function` +Default: `undefined` + +> ℹ️ To ignore files by path please use the [`globOptions.ignore`]((#globoptions) option. + +**webpack.config.js** + +```js +const fs = require('fs').promise; + +module.exports = { + plugins: [ + new CopyPlugin({ + patterns: [ + { + from: 'public/**/*', + filter: async (resourcePath) => { + const data = await fs.promises.readFile(resourcePath); + const content = data.toString(); + + if (content === 'my-custom-content') { + return false; + } + + return true; + }, + }, + ], + }), + ], +}; +``` + #### `toType` Type: `String` diff --git a/src/options.json b/src/options.json index 29ae6b2b..76d620b5 100644 --- a/src/options.json +++ b/src/options.json @@ -17,6 +17,9 @@ "globOptions": { "type": "object" }, + "filter": { + "instanceof": "Function" + }, "toType": { "enum": ["dir", "file", "template"] }, diff --git a/src/processPattern.js b/src/processPattern.js index f750329b..a961a501 100644 --- a/src/processPattern.js +++ b/src/processPattern.js @@ -31,32 +31,46 @@ export default async function processPattern(globalRef, pattern) { return Promise.resolve(); } - return ( - paths - // Exclude directories - .filter((item) => item.dirent.isFile()) - .map((item) => { - const from = item.path; - - logger.debug(`found ${from}`); - - // `globby`/`fast-glob` return the relative path when the path contains special characters on windows - const absoluteFrom = path.resolve(pattern.context, from); - const relativeFrom = pattern.flatten - ? path.basename(absoluteFrom) - : path.relative(pattern.context, absoluteFrom); - let webpackTo = - pattern.toType === 'dir' - ? path.join(pattern.to, relativeFrom) - : pattern.to; - - if (path.isAbsolute(webpackTo)) { - webpackTo = path.relative(output, webpackTo); + const filteredPaths = ( + await Promise.all( + paths.map(async (item) => { + // Exclude directories + if (!item.dirent.isFile()) { + return false; } - logger.log(`determined that '${from}' should write to '${webpackTo}'`); + if (pattern.filter) { + const isFiltered = await pattern.filter(item.path); - return { absoluteFrom, relativeFrom, webpackTo }; + return isFiltered ? item : false; + } + + return item; }) - ); + ) + ).filter((item) => item); + + return filteredPaths.map((item) => { + const from = item.path; + + logger.debug(`found ${from}`); + + // `globby`/`fast-glob` return the relative path when the path contains special characters on windows + const absoluteFrom = path.resolve(pattern.context, from); + const relativeFrom = pattern.flatten + ? path.basename(absoluteFrom) + : path.relative(pattern.context, absoluteFrom); + let webpackTo = + pattern.toType === 'dir' + ? path.join(pattern.to, relativeFrom) + : pattern.to; + + if (path.isAbsolute(webpackTo)) { + webpackTo = path.relative(output, webpackTo); + } + + logger.log(`determined that '${from}' should write to '${webpackTo}'`); + + return { absoluteFrom, relativeFrom, webpackTo }; + }); } diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 0742af57..3ee7e2d4 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -14,7 +14,7 @@ exports[`validate options should throw an error on the "options" 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?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "[""]" value 1`] = ` @@ -37,6 +37,11 @@ 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","filter":"test"}]" value 1`] = ` +"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. + - options.patterns[0].filter should be an instance of function." +`; + 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: @@ -72,7 +77,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?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? } Details: * options.patterns[0].cacheTransform should be one of these: boolean | string | object { directory?, keys? } @@ -112,71 +117,71 @@ 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?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (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?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (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?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "undefined" value 1`] = ` "Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. - options misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; 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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; 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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; 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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; 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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; 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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; 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 misses the property 'patterns'. Should be: - [non-empty string | object { from, to?, context?, globOptions?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; diff --git a/test/filter-option.test.js b/test/filter-option.test.js new file mode 100644 index 00000000..1f43b8d4 --- /dev/null +++ b/test/filter-option.test.js @@ -0,0 +1,56 @@ +import fs from 'fs'; + +import { runEmit } from './helpers/run'; + +describe('"filter" option', () => { + it('should work, copy files and filter some of them', (done) => { + runEmit({ + expectedAssetKeys: [ + '.dottedfile', + 'nested/deep-nested/deepnested.txt', + 'nested/nestedfile.txt', + ], + patterns: [ + { + from: 'directory', + filter: (resourcePath) => { + if (/directoryfile\.txt$/.test(resourcePath)) { + return false; + } + + return true; + }, + }, + ], + }) + .then(done) + .catch(done); + }); + + it('should work, copy files and filter some of them using async function', (done) => { + runEmit({ + expectedAssetKeys: [ + '.dottedfile', + 'nested/deep-nested/deepnested.txt', + 'nested/nestedfile.txt', + ], + patterns: [ + { + from: 'directory', + filter: async (resourcePath) => { + const data = await fs.promises.readFile(resourcePath); + const content = data.toString(); + + if (content === 'new') { + return false; + } + + return true; + }, + }, + ], + }) + .then(done) + .catch(done); + }); +}); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 34b653e7..13f2e345 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -132,6 +132,12 @@ describe('validate options', () => { }, }, ], + [ + { + from: 'test.txt', + filter: () => true, + }, + ], ], failure: [ // eslint-disable-next-line no-undefined @@ -247,6 +253,12 @@ describe('validate options', () => { noErrorOnMissing: 'true', }, ], + [ + { + from: 'test.txt', + filter: 'test', + }, + ], ], }, options: {