Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -874,22 +874,67 @@ class CopyPlugin {
*/
let copiedResult;

try {
copiedResult = await CopyPlugin.glob(
globby,
compiler,
compilation,
logger,
cache,
concurrency,
/** @type {ObjectPattern & { context: string }} */
(pattern),
index,
);
} catch (error) {
compilation.errors.push(/** @type {Error} */ (error));
const fromList = Array.isArray(pattern.from)
? pattern.from
: [pattern.from];

return;
if (fromList.length === 0) {
copiedResult = [];
} else if (fromList.length > 1) {
const results = [];

for (let i = 0; i < fromList.length; i++) {
const from = fromList[i];
const arrayPattern = { ...pattern, from };

try {
const result = await CopyPlugin.glob(
globby,
compiler,
compilation,
logger,
cache,
concurrency,
/** @type {ObjectPattern & { context: string }} */
(arrayPattern),
index * 1000 + i, // Unique index for caching
);

if (result) {
results.push(...result);
}
} catch (error) {
compilation.errors.push(/** @type {Error} */ (error));

// Continue with next from in array
continue;
}
}

copiedResult = results;
} else {
const singlePattern = {
...pattern,
from: /** @type {string} */ (fromList[0]),
};

try {
copiedResult = await CopyPlugin.glob(
globby,
compiler,
compilation,
logger,
cache,
concurrency,
/** @type {ObjectPattern & { context: string }} */
(singlePattern),
index,
);
} catch (error) {
compilation.errors.push(/** @type {Error} */ (error));

return;
}
}

if (!copiedResult) {
Expand Down
19 changes: 15 additions & 4 deletions src/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@
"additionalProperties": false,
"properties": {
"from": {
"type": "string",
"description": "Glob or path from where we copy files.",
"link": "https://github.com/webpack/copy-webpack-plugin#from",
"minLength": 1
"anyOf": [
{
"type": "string",
"minLength": 1
},
{
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
],
"description": "Glob or path (or array of paths) from where we copy files.",
"link": "https://github.com/webpack/copy-webpack-plugin#from"
},
"to": {
"anyOf": [
Expand Down
36 changes: 25 additions & 11 deletions test/__snapshots__/validate-options.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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/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`] = `
Expand Down Expand Up @@ -167,23 +165,39 @@ 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":{"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/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?, noErrorOnMissing? }
Details:
* options.patterns[0].from should be one of these:
non-empty string | [non-empty string, ...]
-> Glob or path (or array of paths) from where we copy files.
-> Read more at https://github.com/webpack/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, ...]"
`;

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/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?, noErrorOnMissing? }
Details:
* options.patterns[0].from should be one of these:
non-empty string | [non-empty string, ...]
-> Glob or path (or array of paths) from where we copy files.
-> Read more at https://github.com/webpack/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, ...]"
`;

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, ...]
-> Glob or path (or array of paths) from where we copy files.
-> Read more at https://github.com/webpack/copy-webpack-plugin#from"
`;

Expand Down
174 changes: 174 additions & 0 deletions test/from-array-option.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import path from "node:path";

import { runEmit } from "./helpers/run";

const FIXTURES_DIR = path.join(__dirname, "fixtures");

const FIXTURES_DIR_NORMALIZED = FIXTURES_DIR.replaceAll("\\", "/");

describe("from option as array", () => {
it("should copy multiple files from array", (done) => {
runEmit({
expectedAssetKeys: ["directoryfile.txt", "file.txt"],
patterns: [
{
from: ["file.txt", "directory/directoryfile.txt"],
},
],
})
.then(done)
.catch(done);
});

it("should copy files from array with absolute paths", (done) => {
runEmit({
expectedAssetKeys: ["directoryfile.txt", "file.txt"],
patterns: [
{
from: [
path.join(FIXTURES_DIR, "file.txt"),
path.join(FIXTURES_DIR, "directory/directoryfile.txt"),
],
},
],
})
.then(done)
.catch(done);
});

it("should copy files from array to specific directory", (done) => {
runEmit({
expectedAssetKeys: ["dist/file.txt", "dist/directoryfile.txt"],
patterns: [
{
from: ["file.txt", "directory/directoryfile.txt"],
to: "dist",
},
],
})
.then(done)
.catch(done);
});

it("should handle missing files with noErrorOnMissing", (done) => {
runEmit({
expectedAssetKeys: ["file.txt"],
patterns: [
{
from: ["file.txt", "nonexistent.txt"],
noErrorOnMissing: true,
},
],
})
.then(done)
.catch(done);
});

it("should error on missing files by default", (done) => {
runEmit({
expectedAssetKeys: ["file.txt"],
expectedErrors: [
new Error(
`unable to locate '${FIXTURES_DIR_NORMALIZED}/nonexistent.txt' glob`,
),
],
patterns: [
{
from: ["file.txt", "nonexistent.txt"],
},
],
})
.then(done)
.catch(done);
});

it("should apply filter to all files in array", (done) => {
runEmit({
expectedAssetKeys: ["file.txt"],
patterns: [
{
from: ["file.txt", "directory/directoryfile.txt"],
filter: (resource) => path.basename(resource) === "file.txt",
},
],
})
.then(done)
.catch(done);
});

it("should apply transform to all files in array", (done) => {
runEmit({
expectedAssetKeys: ["directoryfile.txt", "file.txt"],
expectedAssetContent: {
"file.txt": "transformed: new",
"directoryfile.txt": "transformed: new",
},
patterns: [
{
from: ["file.txt", "directory/directoryfile.txt"],
transform: (content) => `transformed: ${content.toString()}`,
},
],
})
.then(done)
.catch(done);
});

it("should handle empty array gracefully", (done) => {
runEmit({
expectedAssetKeys: [],
patterns: [
{
from: [],
},
],
})
.then(done)
.catch(done);
});

it("should handle single item array (backward compatibility)", (done) => {
runEmit({
expectedAssetKeys: ["file.txt"],
patterns: [
{
from: ["file.txt"],
},
],
})
.then(done)
.catch(done);
});

it("should work with filter in array", (done) => {
runEmit({
expectedAssetKeys: ["file.txt"],
patterns: [
{
from: ["file.txt", "directory/directoryfile.txt"],
filter: (resource) => path.basename(resource) === "file.txt",
},
],
})
.then(done)
.catch(done);
});

it('should copy files when "from" is an array containing a directory', (done) => {
runEmit({
expectedAssetKeys: [
".dottedfile",
"directoryfile.txt",
"nested/deep-nested/deepnested.txt",
"nested/nestedfile.txt",
],
patterns: [
{
from: ["directory"],
},
],
})
.then(done)
.catch(done);
});
});