From ea610bc1a0fa7097f291b0928fb28eb96b0f03af Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Thu, 4 Mar 2021 16:31:36 +0300 Subject: [PATCH] feat: added `priority` option (#590) --- README.md | 36 ++++++++++++ src/index.js | 32 +++++++++-- src/options.json | 3 + .../validate-options.test.js.snap | 49 ++++++++++------ test/priority-option.test.js | 57 +++++++++++++++++++ test/validate-options.test.js | 28 +++++++++ 6 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 test/priority-option.test.js diff --git a/README.md b/README.md index af0b3ea..b20837a 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ module.exports = { | [`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). | +| [`priority`](#priority) | `{Number}` | `0` | Allows you to specify the copy priority. | | [`transform`](#transform) | `{Object}` | `undefined` | Allows to modify the file contents. Enable `transform` caching. You can use `{ transform: {cache: { key: 'my-cache-key' }} }` to invalidate the cache. | | [`noErrorOnMissing`](#noerroronmissing) | `{Boolean}` | `false` | Doesn't generate an error on missing file(s). | | [`info`](#info) | `{Object\|Function}` | `undefined` | Allows to add assets info. | @@ -462,6 +463,41 @@ module.exports = { }; ``` +#### `priority` + +Type: `Number` +Default: `0` + +Allows to specify the priority of copying files with the same destination name. +Files for patterns with higher priority will be copied later. +To overwrite files, the [`force`](#force) option must be enabled. + +**webpack.config.js** + +```js +module.exports = { + plugins: [ + new CopyPlugin({ + patterns: [ + // Сopied second and will overwrite "dir_2/file.txt" + { + from: "dir_1/file.txt", + to: "newfile.txt", + force: true, + priority: 10, + }, + // Сopied first + { + from: "dir_2/file.txt", + to: "newfile.txt", + priority: 5, + }, + ], + }), + ], +}; +``` + #### `transform` Type: `Function|Object` diff --git a/src/index.js b/src/index.js index 7d7d462..ad77115 100644 --- a/src/index.js +++ b/src/index.js @@ -70,6 +70,7 @@ class CopyPlugin { } static async runPattern( + assetMap, compiler, compilation, logger, @@ -313,7 +314,13 @@ class CopyPlugin { path.relative(compiler.context, absoluteFilename) ); - return { absoluteFilename, sourceFilename, filename, toType }; + return { + absoluteFilename, + sourceFilename, + filename, + toType, + priority: pattern.priority || 0, + }; }) ); @@ -322,7 +329,13 @@ class CopyPlugin { try { assets = await Promise.all( files.map(async (file) => { - const { absoluteFilename, sourceFilename, filename, toType } = file; + const { + absoluteFilename, + sourceFilename, + filename, + toType, + priority, + } = file; const info = typeof pattern.info === "function" ? pattern.info(file) || {} @@ -578,6 +591,12 @@ class CopyPlugin { result.filename = normalizePath(result.filename); } + if (!assetMap.has(priority)) { + assetMap.set(priority, []); + } + + assetMap.get(priority).push(result); + // eslint-disable-next-line consistent-return return result; }) @@ -612,13 +631,14 @@ class CopyPlugin { async (unusedAssets, callback) => { logger.log("starting to add additional assets..."); - let assets; + const assetMap = new Map(); try { - assets = await Promise.all( + await Promise.all( this.patterns.map((item, index) => limit(async () => CopyPlugin.runPattern( + assetMap, compiler, compilation, logger, @@ -637,10 +657,12 @@ class CopyPlugin { return; } + const assets = [...assetMap.entries()].sort((a, b) => a[0] - b[0]); + // Avoid writing assets inside `p-limit`, because it creates concurrency. // It could potentially lead to an error - 'Multiple assets emit different content to the same filename' assets - .reduce((acc, val) => acc.concat(val), []) + .reduce((acc, val) => acc.concat(val[1]), []) .filter(Boolean) .forEach((asset) => { const { diff --git a/src/options.json b/src/options.json index 68f7272..725ff76 100644 --- a/src/options.json +++ b/src/options.json @@ -33,6 +33,9 @@ "force": { "type": "boolean" }, + "priority": { + "type": "number" + }, "info": { "anyOf": [ { diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index a08f0ef..fd384cb 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; exports[`validate options should throw an error on the "patterns" option with "[""]" value 1`] = ` @@ -40,7 +40,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":"dir","info":"string"}]" 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } Details: * options.patterns[0].info should be one of these: object { … } | function @@ -53,7 +53,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":"dir","info":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?, globOptions?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } Details: * options.patterns[0].info should be one of these: object { … } | function @@ -88,7 +88,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","transform":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?, globOptions?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } Details: * options.patterns[0].transform should be one of these: function | object { transformer?, cache? } @@ -103,10 +103,25 @@ exports[`validate options should throw an error on the "patterns" option with "[ - options.patterns[0].context should be a string." `; +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","priority":"5"}]" value 1`] = ` +"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. + - options.patterns[0].priority should be a number." +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir","priority":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].priority should be a number." +`; + +exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":"dir"}]" value 1`] = ` +"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema. + - options.patterns[0].priority should be a number." +`; + exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":true,"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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? } + non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? } Details: * options.patterns[0].to should be one of these: string | function @@ -134,71 +149,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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, 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?, filter?, toType?, force?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" + [non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, priority?, info?, transform?, transformPath?, noErrorOnMissing? }, ...] (should not have fewer than 1 item)" `; diff --git a/test/priority-option.test.js b/test/priority-option.test.js new file mode 100644 index 0000000..cd95427 --- /dev/null +++ b/test/priority-option.test.js @@ -0,0 +1,57 @@ +import { run } from "./helpers/run"; + +describe("priority option", () => { + it("should copy without specifying priority option", (done) => { + run({ + expectedAssetKeys: [], + patterns: [ + { + from: "dir (86)/file.txt", + to: "newfile.txt", + force: true, + }, + { + from: "file.txt", + to: "newfile.txt", + force: true, + }, + ], + }) + .then(({ stats }) => { + const { info } = stats.compilation.getAsset("newfile.txt"); + + expect(info.sourceFilename).toEqual("file.txt"); + + done(); + }) + .catch(done); + }); + + it("should copy with specifying priority option", (done) => { + run({ + expectedAssetKeys: [], + patterns: [ + { + from: "dir (86)/file.txt", + to: "newfile.txt", + force: true, + priority: 10, + }, + { + from: "file.txt", + to: "newfile.txt", + force: true, + priority: 5, + }, + ], + }) + .then(({ stats }) => { + const { info } = stats.compilation.getAsset("newfile.txt"); + + expect(info.sourceFilename).toEqual("dir (86)/file.txt"); + + done(); + }) + .catch(done); + }); +}); diff --git a/test/validate-options.test.js b/test/validate-options.test.js index 7b85e15..26ffbdc 100644 --- a/test/validate-options.test.js +++ b/test/validate-options.test.js @@ -133,6 +133,13 @@ describe("validate options", () => { }, }, ], + [ + { + from: "test.txt", + to: "dir", + priority: 5, + }, + ], ], failure: [ // eslint-disable-next-line no-undefined @@ -242,6 +249,27 @@ describe("validate options", () => { filter: "test", }, ], + [ + { + from: "test.txt", + to: "dir", + priority: "5", + }, + ], + [ + { + from: "test.txt", + to: "dir", + priority: () => {}, + }, + ], + [ + { + from: "test.txt", + to: "dir", + priority: true, + }, + ], ], }, options: {