From 3cb90e1485b6d7f2c0343ee5638757e15cb29cef Mon Sep 17 00:00:00 2001 From: Ilya Ryzhenkov Date: Tue, 23 Nov 2021 18:57:07 +0300 Subject: [PATCH] feat: implement support for array values in `from` property. Closes #631 --- README.md | 31 +++++++++++-- src/index.js | 44 ++++++++++++++---- src/options.json | 26 ++++++++--- .../validate-options.test.js.snap | 46 ++++++++++++++----- test/from-option.test.js | 33 +++++++++++++ test/transform-option.test.js | 24 ++++++++++ test/transformAll-option.test.js | 27 +++++++++++ test/validate-options.test.js | 21 +++++++++ 8 files changed, 222 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ac59fc1..91b61d1 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ module.exports = { | Name | Type | Default | Description | | :-------------------------------------: | :------------------: | :---------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`from`](#from) | `{String}` | `undefined` | Glob or path from where we copy files. | +| [`from`](#from) | `{String\|Array}` | `undefined` | Glob or path from where we copy files, or array of such values. | | [`to`](#to) | `{String\|Function}` | `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. | @@ -98,12 +98,12 @@ module.exports = { #### `from` -Type: `String` +Type: `String or Array of strings` Default: `undefined` Glob or path from where we copy files. Globs accept [fast-glob pattern-syntax](https://github.com/mrmlnc/fast-glob#pattern-syntax). -Glob can only be a `string`. +Glob can only be a non-empty `string` or non-empty array of non-empty strings. > ⚠️ Don't use directly `\\` in `from` option if it is a `glob` (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. @@ -177,6 +177,31 @@ module.exports = { The `context` behaves differently depending on what the `from` is (`glob`, `file` or `dir`). More [`examples`](#examples) +##### Using arrays in `from` + +When `from` is specified as an array, it behaves the same as if all array elements were processed separately +using shared options, such as `transform`. The only exception is `transformAll` which receives assets from processed +array elements in a single call, thus allowing to process group of specific files at once. + +```js +module.exports = { + plugins: [ + new CopyPlugin({ + from: ["file.txt", "directory/directoryfile.txt"], + to: "file.txt", + transformAll(assets) { + const result = assets.sort().reduce((accumulator, asset) => { + const content = asset.sourceFilename; + accumulator = `${accumulator}${content}::`; + return accumulator; + }, ""); + return result; + }, + }), + ], +}; +``` + #### `to` Type: `String|Function` diff --git a/src/index.js b/src/index.js index 3647cc1..08d4056 100644 --- a/src/index.js +++ b/src/index.js @@ -633,15 +633,41 @@ class CopyPlugin { let assets; try { - assets = await CopyPlugin.runPattern( - globby, - compiler, - compilation, - logger, - cache, - item, - index - ); + if (item.from instanceof Array) { + if (!item.from.every((from) => typeof from === "string")) { + compilation.errors.push( + new Error( + `Invalid "pattern.from": ${item.from}, every element should be a string"` + ) + ); + } + + assets = [].concat( + ...(await Promise.all( + item.from.map(async (from) => + CopyPlugin.runPattern( + globby, + compiler, + compilation, + logger, + cache, + { ...item, from }, + index + ) + ) + )) + ); + } else { + assets = await CopyPlugin.runPattern( + globby, + compiler, + compilation, + logger, + cache, + item, + index + ); + } } catch (error) { compilation.errors.push(error); diff --git a/src/options.json b/src/options.json index ae9f96d..3872f55 100644 --- a/src/options.json +++ b/src/options.json @@ -5,13 +5,25 @@ "additionalProperties": false, "properties": { "from": { - "type": "string", - "description": "Glob or path from where we copy files.", - "link": "https://github.com/webpack-contrib/copy-webpack-plugin#from", - "minLength": 1 + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + } + ], + "description": "Glob or path from where we copy files, or array of paths or globs.", + "link": "https://github.com/webpack-contrib/copy-webpack-plugin#from" }, "to": { - "anyOf": [ + "oneOf": [ { "type": "string" }, @@ -58,7 +70,7 @@ "link": "https://github.com/webpack-contrib/copy-webpack-plugin#priority" }, "info": { - "anyOf": [ + "oneOf": [ { "type": "object" }, @@ -72,7 +84,7 @@ "transform": { "description": "Allows to modify the file contents.", "link": "https://github.com/webpack-contrib/copy-webpack-plugin#transform", - "anyOf": [ + "oneOf": [ { "instanceof": "Function" }, diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 274df21..936789a 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -38,9 +38,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":"","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 a non-empty string. - -> Glob or path from where we copy files. - -> Read more at https://github.com/webpack-contrib/copy-webpack-plugin#from" + - options.patterns[0].from should be a non-empty string." `; exports[`validate options should throw an error on the "patterns" option with "[{"from":"dir","info":"string"}]" value 1`] = ` @@ -165,25 +163,51 @@ exports[`validate options should throw an error on the "patterns" option with "[ * options.patterns[0].to should be an instance of function." `; +exports[`validate options should throw an error on the "patterns" option with "[{"from":["test1.txt",1],"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[1] should be a non-empty string." +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":[]}]" 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 a non-empty array." +`; + exports[`validate options should throw an error on the "patterns" option with "[{"from":{"glob":"**/*","dot":false},"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 a non-empty string. - -> Glob or path from where we copy files. - -> Read more at https://github.com/webpack-contrib/copy-webpack-plugin#from" + - options.patterns[0] should be one of these: + non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } + Details: + * options.patterns[0].from should be one of these: + non-empty string | [non-empty string, ...] (should not have fewer than 1 item) + -> Glob or path from where we copy files, or array of paths or globs. + -> Read more at https://github.com/webpack-contrib/copy-webpack-plugin#from + Details: + * options.patterns[0].from should be a non-empty string. + * options.patterns[0].from should be an array: + [non-empty string, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "[{"from":true,"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 a non-empty string. - -> Glob or path from where we copy files. - -> Read more at https://github.com/webpack-contrib/copy-webpack-plugin#from" + - options.patterns[0] should be one of these: + non-empty string | object { from, to?, context?, globOptions?, filter?, transformAll?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } + Details: + * options.patterns[0].from should be one of these: + non-empty string | [non-empty string, ...] (should not have fewer than 1 item) + -> Glob or path from where we copy files, or array of paths or globs. + -> Read more at https://github.com/webpack-contrib/copy-webpack-plugin#from + Details: + * options.patterns[0].from should be a non-empty string. + * options.patterns[0].from should be an array: + [non-empty string, ...] (should not have fewer than 1 item)" `; 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[0] misses the property 'from'. Should be: - non-empty string - -> Glob or path from where we copy files. + non-empty string | [non-empty string, ...] (should not have fewer than 1 item) + -> Glob or path from where we copy files, or array of paths or globs. -> Read more at https://github.com/webpack-contrib/copy-webpack-plugin#from" `; diff --git a/test/from-option.test.js b/test/from-option.test.js index 3343079..e70db87 100644 --- a/test/from-option.test.js +++ b/test/from-option.test.js @@ -21,6 +21,18 @@ describe("from option", () => { .then(done) .catch(done); }); + it("should copy an array of files", (done) => { + runEmit({ + expectedAssetKeys: ["file.txt", "directoryfile.txt"], + patterns: [ + { + from: ["file.txt", "directory/directoryfile.txt"], + }, + ], + }) + .then(done) + .catch(done); + }); it('should copy a file when "from" an absolute path', (done) => { runEmit({ @@ -154,6 +166,27 @@ describe("from option", () => { .catch(done); }); + it('should copy files when "from" is an array of directories', (done) => { + runEmit({ + expectedAssetKeys: [ + "file.txt", + "nesteddir/deepnesteddir/deepnesteddir.txt", + "nesteddir/nestedfile.txt", + ".dottedfile", + "directoryfile.txt", + "nested/deep-nested/deepnested.txt", + "nested/nestedfile.txt", + ], + patterns: [ + { + from: ["dir (86)", "directory"], + }, + ], + }) + .then(done) + .catch(done); + }); + it('should copy files when "from" is relative path to context', (done) => { runEmit({ expectedAssetKeys: [ diff --git a/test/transform-option.test.js b/test/transform-option.test.js index 663464f..eb2c644 100644 --- a/test/transform-option.test.js +++ b/test/transform-option.test.js @@ -29,6 +29,30 @@ describe("transform option", () => { .catch(done); }); + it('should transform files when "from" is an array of files', (done) => { + runEmit({ + expectedAssetKeys: ["file.txt", "directoryfile.txt"], + expectedAssetContent: { + "file.txt": "newchanged", + "directoryfile.txt": "newchanged", + }, + patterns: [ + { + from: ["file.txt", "directory/directoryfile.txt"], + transform: { + transformer(content, absoluteFrom) { + expect(absoluteFrom.includes(FIXTURES_DIR)).toBe(true); + + return `${content}changed`; + }, + }, + }, + ], + }) + .then(done) + .catch(done); + }); + it('should transform target path of every when "from" is a directory', (done) => { runEmit({ expectedAssetKeys: [ diff --git a/test/transformAll-option.test.js b/test/transformAll-option.test.js index 93d72f4..c354f55 100644 --- a/test/transformAll-option.test.js +++ b/test/transformAll-option.test.js @@ -51,6 +51,33 @@ describe("transformAll option", () => { .catch(done); }); + it('should transform files when when "from" is an array of files', (done) => { + runEmit({ + expectedAssetKeys: ["file.txt"], + expectedAssetContent: { + "file.txt": "directory/directoryfile.txt::file.txt::", + }, + patterns: [ + { + from: ["file.txt", "directory/directoryfile.txt"], + to: "file.txt", + transformAll(assets) { + const result = assets.sort().reduce((accumulator, asset) => { + const content = asset.sourceFilename; + // eslint-disable-next-line no-param-reassign + accumulator = `${accumulator}${content}::`; + return accumulator; + }, ""); + + return result; + }, + }, + ], + }) + .then(done) + .catch(done); + }); + it("should transform files when async function used", (done) => { runEmit({ expectedAssetKeys: ["file.txt"], diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 607a869..ede2c2f 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -59,6 +59,14 @@ describe("validate options", () => { transform: () => {}, }, ], + [ + { + from: ["test1.txt", "test2.txt"], + to: "dir", + context: "context", + transform: () => {}, + }, + ], [ { from: "test.txt", @@ -165,6 +173,11 @@ describe("validate options", () => { info: "string", }, ], + [ + { + from: [], + }, + ], [ { from: "dir", @@ -233,6 +246,14 @@ describe("validate options", () => { transform: true, }, ], + [ + { + from: ["test1.txt", 1], + to: "dir", + context: "context", + transform: () => {}, + }, + ], [ { from: {