Skip to content

Commit 9bc5416

Browse files
feat: added type Function for the to option (#563)
1 parent 7167645 commit 9bc5416

8 files changed

+481
-49
lines changed

README.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ module.exports = {
7979
| Name | Type | Default | Description |
8080
| :-------------------------------------: | :-------------------------: | :---------------------------------------------: | :---------------------------------------------------------------------------------------------------- |
8181
| [`from`](#from) | `{String}` | `undefined` | Glob or path from where we сopy files. |
82-
| [`to`](#to) | `{String}` | `compiler.options.output` | Output path. |
82+
| [`to`](#to) | `{String\|Function}` | `compiler.options.output` | Output path. |
8383
| [`context`](#context) | `{String}` | `options.context \|\| compiler.options.context` | A path that determines how to interpret the `from` path. |
8484
| [`globOptions`](#globoptions) | `{Object}` | `undefined` | [Options][glob-options] passed to the glob pattern matching library including `ignore` option. |
8585
| [`filter`](#filter) | `{Function}` | `undefined` | Allows to filter copied assets. |
@@ -174,9 +174,11 @@ More [`examples`](#examples)
174174

175175
#### `to`
176176

177-
Type: `String`
177+
Type: `String|Function`
178178
Default: `compiler.options.output`
179179

180+
##### String
181+
180182
Output path.
181183

182184
> ⚠️ Don't use directly `\\` in `to` (i.e `path\to\dest`) option because on UNIX the backslash is a valid character inside a path component, i.e., it's not a separator.
@@ -208,6 +210,53 @@ module.exports = {
208210
};
209211
```
210212

213+
##### Function
214+
215+
Allows to modify the writing path.
216+
217+
> ⚠️ Don't return directly `\\` in `to` (i.e `path\to\newFile`) option because on UNIX the backslash is a valid character inside a path component, i.e., it's not a separator.
218+
> On Windows, the forward slash and the backward slash are both separators.
219+
> Instead please use `/` or `path` methods.
220+
221+
**webpack.config.js**
222+
223+
```js
224+
module.exports = {
225+
plugins: [
226+
new CopyPlugin({
227+
patterns: [
228+
{
229+
from: "src/*.png",
230+
to({ context, absoluteFilename }) {
231+
return "dest/newPath";
232+
},
233+
},
234+
],
235+
}),
236+
],
237+
};
238+
```
239+
240+
**webpack.config.js**
241+
242+
```js
243+
module.exports = {
244+
plugins: [
245+
new CopyPlugin({
246+
patterns: [
247+
{
248+
from: "src/*.png",
249+
to: "dest/",
250+
to({ context, absoluteFilename }) {
251+
return Promise.resolve("dest/newPath");
252+
},
253+
},
254+
],
255+
}),
256+
],
257+
};
258+
```
259+
211260
#### `context`
212261

213262
Type: `String`

src/index.js

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,6 @@ class CopyPlugin {
102102

103103
pattern.fromOrigin = pattern.from;
104104
pattern.from = path.normalize(pattern.from);
105-
pattern.to = path.normalize(
106-
typeof pattern.to !== "undefined" ? pattern.to : ""
107-
);
108105
pattern.compilerContext = compiler.context;
109106
pattern.context = path.normalize(
110107
typeof pattern.context !== "undefined"
@@ -115,26 +112,9 @@ class CopyPlugin {
115112
);
116113

117114
logger.log(
118-
`starting to process a pattern from '${pattern.from}' using '${pattern.context}' context to '${pattern.to}'...`
115+
`starting to process a pattern from '${pattern.from}' using '${pattern.context}' context`
119116
);
120117

121-
const isToDirectory =
122-
path.extname(pattern.to) === "" || pattern.to.slice(-1) === path.sep;
123-
124-
switch (true) {
125-
// if toType already exists
126-
case !!pattern.toType:
127-
break;
128-
case template.test(pattern.to):
129-
pattern.toType = "template";
130-
break;
131-
case isToDirectory:
132-
pattern.toType = "dir";
133-
break;
134-
default:
135-
pattern.toType = "file";
136-
}
137-
138118
if (path.isAbsolute(pattern.from)) {
139119
pattern.absoluteFrom = pattern.from;
140120
} else {
@@ -310,33 +290,64 @@ class CopyPlugin {
310290
return;
311291
}
312292

313-
const files = filteredPaths.map((item) => {
314-
const from = item.path;
293+
const files = await Promise.all(
294+
filteredPaths.map(async (item) => {
295+
const from = item.path;
315296

316-
logger.debug(`found '${from}'`);
297+
logger.debug(`found '${from}'`);
317298

318-
// `globby`/`fast-glob` return the relative path when the path contains special characters on windows
319-
const absoluteFilename = path.resolve(pattern.context, from);
320-
const relativeFrom = pattern.flatten
321-
? path.basename(absoluteFilename)
322-
: path.relative(pattern.context, absoluteFilename);
323-
let filename =
324-
pattern.toType === "dir"
325-
? path.join(pattern.to, relativeFrom)
326-
: pattern.to;
299+
// `globby`/`fast-glob` return the relative path when the path contains special characters on windows
300+
const absoluteFilename = path.resolve(pattern.context, from);
327301

328-
if (path.isAbsolute(filename)) {
329-
filename = path.relative(compiler.options.output.path, filename);
330-
}
302+
pattern.to =
303+
typeof pattern.to !== "function"
304+
? path.normalize(
305+
typeof pattern.to !== "undefined" ? pattern.to : ""
306+
)
307+
: await pattern.to({ context: pattern.context, absoluteFilename });
308+
309+
const isToDirectory =
310+
path.extname(pattern.to) === "" || pattern.to.slice(-1) === path.sep;
311+
312+
switch (true) {
313+
// if toType already exists
314+
case !!pattern.toType:
315+
break;
316+
case template.test(pattern.to):
317+
pattern.toType = "template";
318+
break;
319+
case isToDirectory:
320+
pattern.toType = "dir";
321+
break;
322+
default:
323+
pattern.toType = "file";
324+
}
331325

332-
logger.log(`determined that '${from}' should write to '${filename}'`);
326+
logger.log(
327+
`'to' option '${pattern.to}' determinated as '${pattern.toType}'`
328+
);
333329

334-
const sourceFilename = normalizePath(
335-
path.relative(pattern.compilerContext, absoluteFilename)
336-
);
330+
const relativeFrom = pattern.flatten
331+
? path.basename(absoluteFilename)
332+
: path.relative(pattern.context, absoluteFilename);
333+
let filename =
334+
pattern.toType === "dir"
335+
? path.join(pattern.to, relativeFrom)
336+
: pattern.to;
337337

338-
return { absoluteFilename, sourceFilename, filename };
339-
});
338+
if (path.isAbsolute(filename)) {
339+
filename = path.relative(compiler.options.output.path, filename);
340+
}
341+
342+
logger.log(`determined that '${from}' should write to '${filename}'`);
343+
344+
const sourceFilename = normalizePath(
345+
path.relative(pattern.compilerContext, absoluteFilename)
346+
);
347+
348+
return { absoluteFilename, sourceFilename, filename };
349+
})
350+
);
340351

341352
let assets;
342353

src/options.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
"minLength": 1
1010
},
1111
"to": {
12-
"type": "string"
12+
"anyOf": [
13+
{
14+
"type": "string"
15+
},
16+
{
17+
"instanceof": "Function"
18+
}
19+
]
1320
},
1421
"context": {
1522
"type": "string"

test/CopyPlugin.test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,5 +1246,46 @@ describe("CopyPlugin", () => {
12461246
.then(done)
12471247
.catch(done);
12481248
});
1249+
1250+
it("should logging when 'to' is a function", (done) => {
1251+
const expectedAssetKeys = ["newFile.txt"];
1252+
1253+
run({
1254+
patterns: [
1255+
{
1256+
from: "file.txt",
1257+
to() {
1258+
return "newFile.txt";
1259+
},
1260+
},
1261+
],
1262+
})
1263+
.then(({ compiler, stats }) => {
1264+
const root = path.resolve(__dirname).replace(/\\/g, "/");
1265+
const logs = stats.compilation.logging
1266+
.get("copy-webpack-plugin")
1267+
.map((entry) =>
1268+
entry.args[0].replace(/\\/g, "/").split(root).join(".")
1269+
)
1270+
// TODO remove after drop webpack@4
1271+
.filter(
1272+
(item) =>
1273+
!item.startsWith("created snapshot") &&
1274+
!item.startsWith("creating snapshot") &&
1275+
!item.startsWith("getting cache") &&
1276+
!item.startsWith("missed cache") &&
1277+
!item.startsWith("stored cache") &&
1278+
!item.startsWith("storing cache")
1279+
)
1280+
.sort();
1281+
1282+
expect(
1283+
Array.from(Object.keys(readAssets(compiler, stats))).sort()
1284+
).toEqual(expectedAssetKeys);
1285+
expect({ logs }).toMatchSnapshot("logs");
1286+
})
1287+
.then(done)
1288+
.catch(done);
1289+
});
12491290
});
12501291
});

test/__snapshots__/CopyPlugin.test.js.snap

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ exports[`CopyPlugin cache should work with the "transform" option: warnings 2`]
175175
exports[`CopyPlugin logging should logging when "from" is a directory: logs 1`] = `
176176
Object {
177177
"logs": Array [
178+
"'to' option '.' determinated as 'dir'",
179+
"'to' option '.' determinated as 'dir'",
180+
"'to' option '.' determinated as 'dir'",
181+
"'to' option '.' determinated as 'dir'",
178182
"added './fixtures/directory' as a context dependency",
179183
"added './fixtures/directory/.dottedfile' as a file dependency",
180184
"added './fixtures/directory/directoryfile.txt' as a file dependency",
@@ -202,7 +206,7 @@ Object {
202206
"reading './fixtures/directory/nested/deep-nested/deepnested.txt'...",
203207
"reading './fixtures/directory/nested/nestedfile.txt'...",
204208
"starting to add additional assets...",
205-
"starting to process a pattern from 'directory' using './fixtures' context to '.'...",
209+
"starting to process a pattern from 'directory' using './fixtures' context",
206210
"writing '.dottedfile' from './fixtures/directory/.dottedfile' to compilation assets...",
207211
"writing 'directoryfile.txt' from './fixtures/directory/directoryfile.txt' to compilation assets...",
208212
"writing 'nested/deep-nested/deepnested.txt' from './fixtures/directory/nested/deep-nested/deepnested.txt' to compilation assets...",
@@ -218,6 +222,7 @@ Object {
218222
exports[`CopyPlugin logging should logging when "from" is a file: logs 1`] = `
219223
Object {
220224
"logs": Array [
225+
"'to' option '.' determinated as 'dir'",
221226
"added './fixtures/file.txt' as a file dependency",
222227
"begin globbing './fixtures/file.txt'...",
223228
"determined './fixtures/file.txt' is a file",
@@ -229,7 +234,7 @@ Object {
229234
"read './fixtures/file.txt'",
230235
"reading './fixtures/file.txt'...",
231236
"starting to add additional assets...",
232-
"starting to process a pattern from 'file.txt' using './fixtures' context to '.'...",
237+
"starting to process a pattern from 'file.txt' using './fixtures' context",
233238
"writing 'file.txt' from './fixtures/file.txt' to compilation assets...",
234239
"written 'file.txt' from './fixtures/file.txt' to compilation assets",
235240
],
@@ -239,6 +244,9 @@ Object {
239244
exports[`CopyPlugin logging should logging when "from" is a glob: logs 1`] = `
240245
Object {
241246
"logs": Array [
247+
"'to' option '.' determinated as 'dir'",
248+
"'to' option '.' determinated as 'dir'",
249+
"'to' option '.' determinated as 'dir'",
242250
"added './fixtures/directory' as a context dependency",
243251
"added './fixtures/directory/directoryfile.txt' as a file dependency",
244252
"added './fixtures/directory/nested/deep-nested/deepnested.txt' as a file dependency",
@@ -260,7 +268,7 @@ Object {
260268
"reading './fixtures/directory/nested/deep-nested/deepnested.txt'...",
261269
"reading './fixtures/directory/nested/nestedfile.txt'...",
262270
"starting to add additional assets...",
263-
"starting to process a pattern from 'directory/**' using './fixtures' context to '.'...",
271+
"starting to process a pattern from 'directory/**' using './fixtures' context",
264272
"writing 'directory/directoryfile.txt' from './fixtures/directory/directoryfile.txt' to compilation assets...",
265273
"writing 'directory/nested/deep-nested/deepnested.txt' from './fixtures/directory/nested/deep-nested/deepnested.txt' to compilation assets...",
266274
"writing 'directory/nested/nestedfile.txt' from './fixtures/directory/nested/nestedfile.txt' to compilation assets...",
@@ -271,6 +279,28 @@ Object {
271279
}
272280
`;
273281

282+
exports[`CopyPlugin logging should logging when 'to' is a function: logs 1`] = `
283+
Object {
284+
"logs": Array [
285+
"'to' option 'newFile.txt' determinated as 'file'",
286+
"added './fixtures/file.txt' as a file dependency",
287+
"begin globbing './fixtures/file.txt'...",
288+
"determined './fixtures/file.txt' is a file",
289+
"determined that './fixtures/file.txt' should write to 'newFile.txt'",
290+
"finished to adding additional assets",
291+
"finished to process a pattern from 'file.txt' using './fixtures' context to 'newFile.txt'",
292+
"found './fixtures/file.txt'",
293+
"getting stats for './fixtures/file.txt'...",
294+
"read './fixtures/file.txt'",
295+
"reading './fixtures/file.txt'...",
296+
"starting to add additional assets...",
297+
"starting to process a pattern from 'file.txt' using './fixtures' context",
298+
"writing 'newFile.txt' from './fixtures/file.txt' to compilation assets...",
299+
"written 'newFile.txt' from './fixtures/file.txt' to compilation assets",
300+
],
301+
}
302+
`;
303+
274304
exports[`CopyPlugin stats should work have assets info: assets 1`] = `
275305
Object {
276306
".dottedfile": "dottedfile contents

test/__snapshots__/validate-options.test.js.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,14 @@ exports[`validate options should throw an error on the "patterns" option with "[
9595
9696
exports[`validate options should throw an error on the "patterns" option with "[{"from":"test.txt","to":true,"context":"context"}]" value 1`] = `
9797
"Invalid options object. Copy Plugin has been initialized using an options object that does not match the API schema.
98-
- options.patterns[0].to should be a string."
98+
- options.patterns[0] should be one of these:
99+
non-empty string | object { from, to?, context?, globOptions?, filter?, toType?, force?, flatten?, transform?, cacheTransform?, transformPath?, noErrorOnMissing? }
100+
Details:
101+
* options.patterns[0].to should be one of these:
102+
string | function
103+
Details:
104+
* options.patterns[0].to should be a string.
105+
* options.patterns[0].to should be an instance of function."
99106
`;
100107
101108
exports[`validate options should throw an error on the "patterns" option with "[{"from":{"glob":"**/*","dot":false},"to":"dir","context":"context"}]" value 1`] = `

0 commit comments

Comments
 (0)