Skip to content

Commit f2c1bc8

Browse files
authoredMar 3, 2021
refactor: template strings in to option (#589)
BREAKING CHANGE: placeholders for the `to` option were changed
1 parent 8d5ab9a commit f2c1bc8

11 files changed

+423
-256
lines changed
 

‎README.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ module.exports = {
200200
},
201201
{
202202
from: "**/*",
203-
to: "[path][name].[contenthash].[ext]",
203+
to: "[path][name].[contenthash][ext]",
204204
},
205205
],
206206
}),
@@ -371,11 +371,11 @@ Sometimes it is hard to say what is `to`, example `path/to/dir-with.ext`.
371371
If you want to copy files in directory you need use `dir` option.
372372
We try to automatically determine the `type` so you most likely do not need this option.
373373

374-
| Name | Type | Default | Description |
375-
| :--------------: | :--------: | :---------: | :------------------------------------------------------------------------------------------------- |
376-
| **`'dir'`** | `{String}` | `undefined` | If `to` has no extension or ends on `'/'` |
377-
| **`'file'`** | `{String}` | `undefined` | If `to` is not a directory and is not a template |
378-
| **`'template'`** | `{String}` | `undefined` | If `to` contains [a template pattern](https://github.com/webpack-contrib/file-loader#placeholders) |
374+
| Name | Type | Default | Description |
375+
| :--------------: | :--------: | :---------: | :--------------------------------------------------------------------------------------------------- |
376+
| **`'dir'`** | `{String}` | `undefined` | If `to` has no extension or ends on `'/'` |
377+
| **`'file'`** | `{String}` | `undefined` | If `to` is not a directory and is not a template |
378+
| **`'template'`** | `{String}` | `undefined` | If `to` contains [a template pattern](https://webpack.js.org/configuration/output/#template-strings) |
379379

380380
##### `'dir'`
381381

@@ -428,7 +428,7 @@ module.exports = {
428428
patterns: [
429429
{
430430
from: "src/",
431-
to: "dest/[name].[hash].[ext]",
431+
to: "dest/[name].[contenthash][ext]",
432432
toType: "template",
433433
},
434434
],
@@ -995,7 +995,7 @@ module.exports = {
995995
patterns: [
996996
{
997997
from: "src/**/*",
998-
to: "[name].[ext]",
998+
to: "[name][ext]",
999999
},
10001000
],
10011001
}),

‎package-lock.json

+155-128
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"fast-glob": "^3.2.5",
4848
"glob-parent": "^5.1.1",
4949
"globby": "^11.0.2",
50-
"loader-utils": "^2.0.0",
5150
"normalize-path": "^3.0.0",
5251
"p-limit": "^3.0.2",
5352
"schema-utils": "^3.0.0",

‎src/index.js

+42-34
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { validate } from "schema-utils";
55
import pLimit from "p-limit";
66
import globby from "globby";
77
import serialize from "serialize-javascript";
8-
import loaderUtils from "loader-utils";
98
import normalizePath from "normalize-path";
109
import globParent from "glob-parent";
1110
import fastGlob from "fast-glob";
@@ -15,7 +14,7 @@ import { version } from "../package.json";
1514
import schema from "./options.json";
1615
import { readFile, stat } from "./utils/promisify";
1716

18-
const template = /(\[ext\])|(\[name\])|(\[path\])|(\[folder\])|(\[emoji(?::(\d+))?\])|(\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\])|(\[\d+\])/;
17+
const template = /\[\\*([\w:]+)\\*\]/i;
1918

2019
class CopyPlugin {
2120
constructor(options = {}) {
@@ -528,40 +527,57 @@ class CopyPlugin {
528527
`interpolating template '${filename}' for '${sourceFilename}'...`
529528
);
530529

531-
// If it doesn't have an extension, remove it from the pattern
532-
// ie. [name].[ext] or [name][ext] both become [name]
533-
if (!path.extname(absoluteFilename)) {
534-
// eslint-disable-next-line no-param-reassign
535-
result.filename = result.filename.replace(/\.?\[ext]/g, "");
530+
const { outputOptions } = compilation;
531+
const {
532+
hashDigest,
533+
hashDigestLength,
534+
hashFunction,
535+
hashSalt,
536+
} = outputOptions;
537+
const hash = compiler.webpack.util.createHash(hashFunction);
538+
539+
if (hashSalt) {
540+
hash.update(hashSalt);
536541
}
537542

538-
// eslint-disable-next-line no-param-reassign
539-
result.immutable = /\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi.test(
540-
result.filename
541-
);
542-
543-
// eslint-disable-next-line no-param-reassign
544-
result.filename = loaderUtils.interpolateName(
545-
{ resourcePath: absoluteFilename },
546-
result.filename,
547-
{
548-
content: result.source.source(),
549-
context: pattern.context,
550-
}
543+
hash.update(result.source.source());
544+
545+
const fullContentHash = hash.digest(hashDigest);
546+
const contentHash = fullContentHash.slice(0, hashDigestLength);
547+
const ext = path.extname(result.sourceFilename);
548+
const base = path.basename(result.sourceFilename);
549+
const name = base.slice(0, base.length - ext.length);
550+
const data = {
551+
filename: normalizePath(
552+
path.relative(pattern.context, absoluteFilename)
553+
),
554+
contentHash,
555+
chunk: {
556+
name,
557+
id: result.sourceFilename,
558+
hash: contentHash,
559+
contentHash,
560+
},
561+
};
562+
const {
563+
path: interpolatedFilename,
564+
info: assetInfo,
565+
} = compilation.getPathWithInfo(
566+
normalizePath(result.filename),
567+
data
551568
);
552569

553-
// Bug in `loader-utils`, package convert `\\` to `/`, need fix in loader-utils
554-
// eslint-disable-next-line no-param-reassign
555-
result.filename = path.normalize(result.filename);
570+
result.info = { ...result.info, ...assetInfo };
571+
result.filename = interpolatedFilename;
556572

557573
logger.log(
558574
`interpolated template '${filename}' for '${sourceFilename}'`
559575
);
576+
} else {
577+
// eslint-disable-next-line no-param-reassign
578+
result.filename = normalizePath(result.filename);
560579
}
561580

562-
// eslint-disable-next-line no-param-reassign
563-
result.filename = normalizePath(result.filename);
564-
565581
// eslint-disable-next-line consistent-return
566582
return result;
567583
})
@@ -641,10 +657,6 @@ class CopyPlugin {
641657
if (force) {
642658
const info = { copied: true, sourceFilename };
643659

644-
if (asset.immutable) {
645-
info.immutable = true;
646-
}
647-
648660
logger.log(
649661
`force updating '${filename}' from '${absoluteFilename}' to compilation assets, because it already exists...`
650662
);
@@ -670,10 +682,6 @@ class CopyPlugin {
670682

671683
const info = { copied: true, sourceFilename };
672684

673-
if (asset.immutable) {
674-
info.immutable = true;
675-
}
676-
677685
logger.log(
678686
`writing '${filename}' from '${absoluteFilename}' to compilation assets...`
679687
);

‎test/CopyPlugin.test.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -310,17 +310,17 @@ describe("CopyPlugin", () => {
310310
expect.assertions(5);
311311

312312
const expectedAssetKeys = [
313-
"directoryfile.5d7817ed5bc246756d73d6a4c8e94c33.txt",
314-
".dottedfile.5e294e270db6734a42f014f0dd18d9ac",
315-
"nested/nestedfile.31d6cfe0d16ae931b73c59d7e0c089c0.txt",
316-
"nested/deep-nested/deepnested.31d6cfe0d16ae931b73c59d7e0c089c0.txt",
313+
"directoryfile.5d7817ed5bc246756d73.txt",
314+
".dottedfile.5e294e270db6734a42f0",
315+
"nested/nestedfile.31d6cfe0d16ae931b73c.txt",
316+
"nested/deep-nested/deepnested.31d6cfe0d16ae931b73c.txt",
317317
];
318318

319319
run({
320320
preCopy: {
321321
additionalAssets: [
322322
{
323-
name: "directoryfile.5d7817ed5bc246756d73d6a4c8e94c33.txt",
323+
name: "directoryfile.5d7817ed5bc246756d73.txt",
324324
data: "Content",
325325
info: { custom: true },
326326
},
@@ -330,7 +330,7 @@ describe("CopyPlugin", () => {
330330
patterns: [
331331
{
332332
from: "directory",
333-
to: "[path][name].[contenthash].[ext]",
333+
to: "[path][name].[contenthash][ext]",
334334
force: true,
335335
},
336336
],
@@ -341,7 +341,7 @@ describe("CopyPlugin", () => {
341341

342342
expect(info.immutable).toBe(true);
343343

344-
if (name === "directoryfile.5d7817ed5bc246756d73d6a4c8e94c33.txt") {
344+
if (name === "directoryfile.5d7817ed5bc246756d73.txt") {
345345
expect(info.immutable).toBe(true);
346346
}
347347
}

‎test/from-option.test.js

+32-24
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import path from "path";
22

33
import { runEmit } from "./helpers/run";
4+
import { getCompiler } from "./helpers";
45

56
const FIXTURES_DIR_NORMALIZED = path
67
.join(__dirname, "fixtures")
78
.replace(/\\/g, "/");
89

910
describe("from option", () => {
1011
describe("is a file", () => {
11-
it("should move a file", (done) => {
12+
it("should copy a file", (done) => {
1213
runEmit({
1314
expectedAssetKeys: ["file.txt"],
1415
patterns: [
@@ -21,7 +22,7 @@ describe("from option", () => {
2122
.catch(done);
2223
});
2324

24-
it('should move a file when "from" an absolute path', (done) => {
25+
it('should copy a file when "from" an absolute path', (done) => {
2526
runEmit({
2627
expectedAssetKeys: ["file.txt"],
2728
patterns: [
@@ -34,7 +35,7 @@ describe("from option", () => {
3435
.catch(done);
3536
});
3637

37-
it("should move a file from nesting directory", (done) => {
38+
it("should copy a file from nesting directory", (done) => {
3839
runEmit({
3940
expectedAssetKeys: ["directoryfile.txt"],
4041
patterns: [
@@ -47,7 +48,7 @@ describe("from option", () => {
4748
.catch(done);
4849
});
4950

50-
it('should move a file from nesting directory when "from" an absolute path', (done) => {
51+
it('should copy a file from nesting directory when "from" an absolute path', (done) => {
5152
runEmit({
5253
expectedAssetKeys: ["directoryfile.txt"],
5354
patterns: [
@@ -63,7 +64,7 @@ describe("from option", () => {
6364
.catch(done);
6465
});
6566

66-
it("should move a file (symbolic link)", (done) => {
67+
it("should copy a file (symbolic link)", (done) => {
6768
runEmit({
6869
symlink: true,
6970
expectedErrors:
@@ -105,7 +106,7 @@ describe("from option", () => {
105106
});
106107

107108
describe("is a directory", () => {
108-
it("should move files", (done) => {
109+
it("should copy files", (done) => {
109110
runEmit({
110111
expectedAssetKeys: [
111112
".dottedfile",
@@ -123,7 +124,7 @@ describe("from option", () => {
123124
.catch(done);
124125
});
125126

126-
it('should move files when "from" is current directory', (done) => {
127+
it('should copy files when "from" is current directory', (done) => {
127128
runEmit({
128129
expectedAssetKeys: [
129130
".file.txt",
@@ -153,7 +154,7 @@ describe("from option", () => {
153154
.catch(done);
154155
});
155156

156-
it('should move files when "from" is relative path to context', (done) => {
157+
it('should copy files when "from" is relative path to context', (done) => {
157158
runEmit({
158159
expectedAssetKeys: [
159160
".file.txt",
@@ -183,7 +184,7 @@ describe("from option", () => {
183184
.catch(done);
184185
});
185186

186-
it("should move files with a forward slash", (done) => {
187+
it("should copy files with a forward slash", (done) => {
187188
runEmit({
188189
expectedAssetKeys: [
189190
".dottedfile",
@@ -201,7 +202,7 @@ describe("from option", () => {
201202
.catch(done);
202203
});
203204

204-
it("should move files from symbolic link", (done) => {
205+
it("should copy files from symbolic link", (done) => {
205206
runEmit({
206207
// Windows doesn't support symbolic link
207208
symlink: true,
@@ -227,7 +228,7 @@ describe("from option", () => {
227228
.catch(done);
228229
});
229230

230-
it("should move files when 'from' is a absolute path", (done) => {
231+
it("should copy files when 'from' is a absolute path", (done) => {
231232
runEmit({
232233
expectedAssetKeys: [
233234
".dottedfile",
@@ -245,7 +246,7 @@ describe("from option", () => {
245246
.catch(done);
246247
});
247248

248-
it("should move files when 'from' with special characters", (done) => {
249+
it("should copy files when 'from' with special characters", (done) => {
249250
runEmit({
250251
expectedAssetKeys: [
251252
"directoryfile.txt",
@@ -263,7 +264,7 @@ describe("from option", () => {
263264
.catch(done);
264265
});
265266

266-
it("should move files from nested directory", (done) => {
267+
it("should copy files from nested directory", (done) => {
267268
runEmit({
268269
expectedAssetKeys: ["deep-nested/deepnested.txt", "nestedfile.txt"],
269270
patterns: [
@@ -276,7 +277,7 @@ describe("from option", () => {
276277
.catch(done);
277278
});
278279

279-
it("should move files from nested directory with an absolute path", (done) => {
280+
it("should copy files from nested directory with an absolute path", (done) => {
280281
runEmit({
281282
expectedAssetKeys: ["deep-nested/deepnested.txt", "nestedfile.txt"],
282283
patterns: [
@@ -309,7 +310,7 @@ describe("from option", () => {
309310
});
310311

311312
describe("is a glob", () => {
312-
it("should move files", (done) => {
313+
it("should copy files", (done) => {
313314
runEmit({
314315
expectedAssetKeys: ["file.txt"],
315316
patterns: [
@@ -322,7 +323,7 @@ describe("from option", () => {
322323
.catch(done);
323324
});
324325

325-
it("should move files when a glob contains absolute path", (done) => {
326+
it("should copy files when a glob contains absolute path", (done) => {
326327
runEmit({
327328
expectedAssetKeys: ["file.txt"],
328329
patterns: [
@@ -335,7 +336,7 @@ describe("from option", () => {
335336
.catch(done);
336337
});
337338

338-
it("should move files using globstar", (done) => {
339+
it("should copy files using globstar", (done) => {
339340
runEmit({
340341
expectedAssetKeys: [
341342
"[(){}[]!+@escaped-test^$]/hello.txt",
@@ -363,7 +364,7 @@ describe("from option", () => {
363364
.catch(done);
364365
});
365366

366-
it("should move files using globstar and contains an absolute path", (done) => {
367+
it("should copy files using globstar and contains an absolute path", (done) => {
367368
runEmit({
368369
expectedAssetKeys: [
369370
"[(){}[]!+@escaped-test^$]/hello.txt",
@@ -388,8 +389,15 @@ describe("from option", () => {
388389
.catch(done);
389390
});
390391

391-
it("should move files in nested directory using globstar", (done) => {
392+
it("should copy files in nested directory using globstar", (done) => {
393+
const compiler = getCompiler({
394+
output: {
395+
hashDigestLength: 6,
396+
},
397+
});
398+
392399
runEmit({
400+
compiler,
393401
expectedAssetKeys: [
394402
"nested/[(){}[]!+@escaped-test^$]/hello-31d6cf.txt",
395403
"nested/binextension-31d6cf.bin",
@@ -409,15 +417,15 @@ describe("from option", () => {
409417
patterns: [
410418
{
411419
from: "**/*",
412-
to: "nested/[path][name]-[hash:6].[ext]",
420+
to: "nested/[path][name]-[contenthash][ext]",
413421
},
414422
],
415423
})
416424
.then(done)
417425
.catch(done);
418426
});
419427

420-
it("should move files from nested directory", (done) => {
428+
it("should copy files from nested directory", (done) => {
421429
runEmit({
422430
expectedAssetKeys: ["directory/directoryfile.txt"],
423431
patterns: [
@@ -430,7 +438,7 @@ describe("from option", () => {
430438
.catch(done);
431439
});
432440

433-
it("should move files from nested directory #2", (done) => {
441+
it("should copy files from nested directory #2", (done) => {
434442
runEmit({
435443
expectedAssetKeys: [
436444
"directory/directoryfile.txt",
@@ -447,7 +455,7 @@ describe("from option", () => {
447455
.catch(done);
448456
});
449457

450-
it("should move files using bracketed glob", (done) => {
458+
it("should copy files using bracketed glob", (done) => {
451459
runEmit({
452460
expectedAssetKeys: [
453461
"directory/directoryfile.txt",
@@ -466,7 +474,7 @@ describe("from option", () => {
466474
.catch(done);
467475
});
468476

469-
it("should move files (symbolic link)", (done) => {
477+
it("should copy files (symbolic link)", (done) => {
470478
runEmit({
471479
// Windows doesn't support symbolic link
472480
symlink: true,

‎test/globOptions-option.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const FIXTURES_DIR_NORMALIZED = path
88

99
describe("globOptions option", () => {
1010
// Expected behavior from `globby`/`fast-glob`
11-
it('should move files exclude dot files when "from" is a directory', (done) => {
11+
it('should copy files exclude dot files when "from" is a directory', (done) => {
1212
runEmit({
1313
expectedAssetKeys: [".file.txt"],
1414
patterns: [
@@ -24,7 +24,7 @@ describe("globOptions option", () => {
2424
.catch(done);
2525
});
2626

27-
it('should move files exclude dot files when "from" is a directory', (done) => {
27+
it('should copy files exclude dot files when "from" is a directory', (done) => {
2828
runEmit({
2929
expectedAssetKeys: [
3030
"directoryfile.txt",
@@ -44,7 +44,7 @@ describe("globOptions option", () => {
4444
.catch(done);
4545
});
4646

47-
it('should move files exclude dot files when "from" is a glob', (done) => {
47+
it('should copy files exclude dot files when "from" is a glob', (done) => {
4848
runEmit({
4949
expectedAssetKeys: ["file.txt"],
5050
patterns: [
@@ -60,7 +60,7 @@ describe("globOptions option", () => {
6060
.catch(done);
6161
});
6262

63-
it("should move files include dot files", (done) => {
63+
it("should copy files include dot files", (done) => {
6464
runEmit({
6565
expectedAssetKeys: [".file.txt", "file.txt"],
6666
patterns: [
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
class BreakContenthashPlugin {
2+
constructor(options = {}) {
3+
this.options = options.options || {};
4+
}
5+
6+
apply(compiler) {
7+
const plugin = { name: "BrokeContenthashPlugin" };
8+
9+
compiler.hooks.thisCompilation.tap(plugin, (compilation) => {
10+
compilation.hooks.processAssets.tapAsync(
11+
{
12+
name: "broken-contenthash-webpack-plugin",
13+
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
14+
},
15+
(unusedAssets, callback) => {
16+
this.options.targetAssets.forEach(({ name, newName, newHash }) => {
17+
const asset = compilation.getAsset(name);
18+
19+
compilation.updateAsset(asset.name, asset.source, {
20+
...asset.info,
21+
contenthash: newHash,
22+
});
23+
compilation.renameAsset(asset.name, newName);
24+
});
25+
26+
callback();
27+
}
28+
);
29+
});
30+
}
31+
}
32+
33+
export default BreakContenthashPlugin;

‎test/helpers/run.js

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import CopyPlugin from "../../src/index";
66

77
import ChildCompilerPlugin from "./ChildCompiler";
88
import PreCopyPlugin from "./PreCopyPlugin";
9+
import BreakContenthashPlugin from "./BreakContenthashPlugin";
910

1011
import removeIllegalCharacterForWindows from "./removeIllegalCharacterForWindows";
1112

@@ -49,6 +50,12 @@ function run(opts) {
4950
new PreCopyPlugin({ options: opts.preCopy }).apply(compiler);
5051
}
5152

53+
if (opts.breakContenthash) {
54+
new BreakContenthashPlugin({ options: opts.breakContenthash }).apply(
55+
compiler
56+
);
57+
}
58+
5259
new CopyPlugin({ patterns: opts.patterns, options: opts.options }).apply(
5360
compiler
5461
);

‎test/to-option.test.js

+128-43
Large diffs are not rendered by default.

‎test/toType-option.test.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const FIXTURES_DIR_NORMALIZED = path
77
.replace(/\\/g, "/");
88

99
describe("toType option", () => {
10-
it("should move a file to a new file", (done) => {
10+
it("should copy a file to a new file", (done) => {
1111
runEmit({
1212
expectedAssetKeys: ["new-file.txt"],
1313
patterns: [
@@ -22,7 +22,7 @@ describe("toType option", () => {
2222
.catch(done);
2323
});
2424

25-
it("should move a file to a new directory", (done) => {
25+
it("should copy a file to a new directory", (done) => {
2626
runEmit({
2727
expectedAssetKeys: ["new-file.txt/file.txt"],
2828
patterns: [
@@ -37,16 +37,16 @@ describe("toType option", () => {
3737
.catch(done);
3838
});
3939

40-
it("should move a file to a new directory", (done) => {
40+
it("should copy a file to a new directory", (done) => {
4141
runEmit({
4242
expectedAssetKeys: [
43-
"directory/directorynew-directoryfile.txt.5d7817ed5bc246756d73d6a4c8e94c33.5d7817ed5bc246756d73d6a4c8e94c33.22af645d.22af645d.txt",
43+
"directory/directoryfile.txt-new-directoryfile.txt.5d7817ed5bc246756d73.ac7f6fcb65ddfcc43b2c-ac7f6fcb65ddfcc43b2c.txt",
4444
],
4545
patterns: [
4646
{
4747
from: "directory/directoryfile.*",
4848
to:
49-
"[path][folder]new-[name].[ext].[hash].[contenthash].[md5:contenthash:hex:8].[md5:hash:hex:8].txt",
49+
"[path][base]-new-[name][ext].[contenthash].[hash]-[fullhash][ext]",
5050
toType: "template",
5151
},
5252
],
@@ -55,7 +55,7 @@ describe("toType option", () => {
5555
.catch(done);
5656
});
5757

58-
it("should move a file to a new file with no extension", (done) => {
58+
it("should copy a file to a new file with no extension", (done) => {
5959
runEmit({
6060
expectedAssetKeys: ["newname"],
6161
patterns: [
@@ -70,7 +70,7 @@ describe("toType option", () => {
7070
.catch(done);
7171
});
7272

73-
it("should move a file to a new directory with an extension", (done) => {
73+
it("should copy a file to a new directory with an extension", (done) => {
7474
runEmit({
7575
expectedAssetKeys: ["newdirectory.ext/file.txt"],
7676
patterns: [

0 commit comments

Comments
 (0)
Please sign in to comment.