Skip to content

Commit 9bc5416

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

8 files changed

+481
-49
lines changed
 

‎README.md

+51-2
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

+53-42
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

+8-1
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

+41
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

+33-3
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

+8-1
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`] = `

‎test/to-option.test.js

+281
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,285 @@ describe("to option", () => {
478478
.catch(done);
479479
});
480480
});
481+
482+
describe("to option as function", () => {
483+
it('should transform target path when "from" is a file', (done) => {
484+
runEmit({
485+
expectedAssetKeys: ["subdir/test.txt"],
486+
patterns: [
487+
{
488+
from: "file.txt",
489+
to({ context, absoluteFilename }) {
490+
expect(absoluteFilename).toBe(
491+
path.join(FIXTURES_DIR, "file.txt")
492+
);
493+
494+
const targetPath = path.relative(context, absoluteFilename);
495+
496+
return targetPath.replace("file.txt", "subdir/test.txt");
497+
},
498+
},
499+
],
500+
})
501+
.then(done)
502+
.catch(done);
503+
});
504+
505+
it('should transform target path of every when "from" is a directory', (done) => {
506+
runEmit({
507+
expectedAssetKeys: [
508+
"../.dottedfile",
509+
"../deepnested.txt",
510+
"../directoryfile.txt",
511+
"../nestedfile.txt",
512+
],
513+
patterns: [
514+
{
515+
from: "directory",
516+
toType: "file",
517+
to({ context, absoluteFilename }) {
518+
expect(
519+
absoluteFilename.includes(path.join(FIXTURES_DIR, "directory"))
520+
).toBe(true);
521+
522+
const targetPath = path.relative(context, absoluteFilename);
523+
524+
return path.resolve(__dirname, path.basename(targetPath));
525+
},
526+
},
527+
],
528+
})
529+
.then(done)
530+
.catch(done);
531+
});
532+
533+
it('should transform target path of every file when "from" is a glob', (done) => {
534+
runEmit({
535+
expectedAssetKeys: [
536+
"../deepnested.txt.tst",
537+
"../directoryfile.txt.tst",
538+
"../nestedfile.txt.tst",
539+
],
540+
patterns: [
541+
{
542+
from: "directory/**/*",
543+
to({ context, absoluteFilename }) {
544+
expect(absoluteFilename.includes(FIXTURES_DIR)).toBe(true);
545+
546+
const targetPath = path.relative(context, absoluteFilename);
547+
548+
return path.resolve(
549+
__dirname,
550+
`${path.basename(targetPath)}.tst`
551+
);
552+
},
553+
},
554+
],
555+
})
556+
.then(done)
557+
.catch(done);
558+
});
559+
560+
it("should transform target path when function return Promise", (done) => {
561+
runEmit({
562+
expectedAssetKeys: ["../file.txt"],
563+
patterns: [
564+
{
565+
from: "file.txt",
566+
to({ context, absoluteFilename }) {
567+
expect(absoluteFilename.includes(FIXTURES_DIR)).toBe(true);
568+
569+
const targetPath = path.relative(context, absoluteFilename);
570+
571+
return new Promise((resolve) => {
572+
resolve(path.resolve(__dirname, path.basename(targetPath)));
573+
});
574+
},
575+
},
576+
],
577+
})
578+
.then(done)
579+
.catch(done);
580+
});
581+
582+
it("should transform target path when async function used", (done) => {
583+
runEmit({
584+
expectedAssetKeys: ["../file.txt"],
585+
patterns: [
586+
{
587+
from: "file.txt",
588+
async to({ context, absoluteFilename }) {
589+
expect(absoluteFilename.includes(FIXTURES_DIR)).toBe(true);
590+
591+
const targetPath = path.relative(context, absoluteFilename);
592+
593+
const newPath = await new Promise((resolve) => {
594+
resolve(path.resolve(__dirname, path.basename(targetPath)));
595+
});
596+
597+
return newPath;
598+
},
599+
},
600+
],
601+
})
602+
.then(done)
603+
.catch(done);
604+
});
605+
606+
it("should warn when function throw error", (done) => {
607+
runEmit({
608+
expectedAssetKeys: [],
609+
expectedErrors: [new Error("a failure happened")],
610+
patterns: [
611+
{
612+
from: "file.txt",
613+
to() {
614+
throw new Error("a failure happened");
615+
},
616+
},
617+
],
618+
})
619+
.then(done)
620+
.catch(done);
621+
});
622+
623+
it("should warn when Promise was rejected", (done) => {
624+
runEmit({
625+
expectedAssetKeys: [],
626+
expectedErrors: [new Error("a failure happened")],
627+
patterns: [
628+
{
629+
from: "file.txt",
630+
to() {
631+
return new Promise((resolve, reject) => {
632+
return reject(new Error("a failure happened"));
633+
});
634+
},
635+
},
636+
],
637+
})
638+
.then(done)
639+
.catch(done);
640+
});
641+
642+
it("should warn when async function throw error", (done) => {
643+
runEmit({
644+
expectedAssetKeys: [],
645+
expectedErrors: [new Error("a failure happened")],
646+
patterns: [
647+
{
648+
from: "file.txt",
649+
async to() {
650+
await new Promise((resolve, reject) => {
651+
reject(new Error("a failure happened"));
652+
});
653+
},
654+
},
655+
],
656+
})
657+
.then(done)
658+
.catch(done);
659+
});
660+
661+
it("should transform target path of every file in glob after applying template", (done) => {
662+
runEmit({
663+
expectedAssetKeys: [
664+
"transformed/directory/directoryfile-5d7817.txt",
665+
"transformed/directory/nested/deep-nested/deepnested-31d6cf.txt",
666+
"transformed/directory/nested/nestedfile-31d6cf.txt",
667+
],
668+
patterns: [
669+
{
670+
from: "directory/**/*",
671+
to({ absoluteFilename }) {
672+
expect(absoluteFilename.includes(FIXTURES_DIR)).toBe(true);
673+
674+
return "transformed/[path][name]-[hash:6].[ext]";
675+
},
676+
},
677+
],
678+
})
679+
.then(done)
680+
.catch(done);
681+
});
682+
683+
it("should move files", (done) => {
684+
runEmit({
685+
expectedAssetKeys: ["txt"],
686+
patterns: [
687+
{
688+
from: "directory/nested/deep-nested",
689+
toType: "file",
690+
to({ absoluteFilename }) {
691+
const mathes = absoluteFilename.match(/\.([^.]*)$/);
692+
const [, res] = mathes;
693+
const target = res;
694+
695+
return target;
696+
},
697+
},
698+
],
699+
})
700+
.then(done)
701+
.catch(done);
702+
});
703+
704+
it("should move files to a non-root directory", (done) => {
705+
runEmit({
706+
expectedAssetKeys: ["nested/txt"],
707+
patterns: [
708+
{
709+
from: "directory/nested/deep-nested",
710+
toType: "file",
711+
to({ absoluteFilename }) {
712+
const mathes = absoluteFilename.match(/\.([^.]*)$/);
713+
const [, res] = mathes;
714+
const target = `nested/${res}`;
715+
716+
return target;
717+
},
718+
},
719+
],
720+
})
721+
.then(done)
722+
.catch(done);
723+
});
724+
725+
it("should move files", (done) => {
726+
runEmit({
727+
expectedAssetKeys: [
728+
"deep-nested-deepnested.txt",
729+
"directoryfile.txt",
730+
"nested-nestedfile.txt",
731+
],
732+
patterns: [
733+
{
734+
from: "**/*",
735+
context: "directory",
736+
to({ context, absoluteFilename }) {
737+
const targetPath = path.relative(context, absoluteFilename);
738+
const pathSegments = path.parse(targetPath);
739+
const result = [];
740+
741+
if (pathSegments.root) {
742+
result.push(pathSegments.root);
743+
}
744+
745+
if (pathSegments.dir) {
746+
result.push(pathSegments.dir.split(path.sep).pop());
747+
}
748+
749+
if (pathSegments.base) {
750+
result.push(pathSegments.base);
751+
}
752+
753+
return result.join("-");
754+
},
755+
},
756+
],
757+
})
758+
.then(done)
759+
.catch(done);
760+
});
761+
});
481762
});

‎test/validate-options.test.js

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ describe("validate options", () => {
2929
to: "dir",
3030
},
3131
],
32+
[
33+
{
34+
from: "test.txt",
35+
to: () => {},
36+
},
37+
],
3238
[
3339
{
3440
from: "test.txt",

0 commit comments

Comments
 (0)
Please sign in to comment.