1 | const crypto = require('crypto');
|
2 | const fs = require('fs');
|
3 |
|
4 | function OutputHash({ validateOutput = false, validateOutputRegex = /^.*$/ } = {}) {
|
5 | this.validateOutput = validateOutput;
|
6 | this.validateOutputRegex = validateOutputRegex;
|
7 | }
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | function replaceStringInAsset(asset, source, target) {
|
13 | const sourceRE = new RegExp(source, 'g');
|
14 |
|
15 | if (typeof asset === 'string') {
|
16 | return asset.replace(sourceRE, target);
|
17 | }
|
18 |
|
19 |
|
20 | if ('_source' in asset) {
|
21 | asset._source = replaceStringInAsset(asset._source, source, target);
|
22 | return asset;
|
23 | }
|
24 |
|
25 |
|
26 | if ('_cachedSource' in asset) {
|
27 | asset._cachedSource = asset.source().replace(sourceRE, target);
|
28 | return asset;
|
29 | }
|
30 |
|
31 |
|
32 | if ('_value' in asset) {
|
33 | asset._value = asset.source().replace(sourceRE, target);
|
34 | return asset;
|
35 | }
|
36 |
|
37 |
|
38 | if ('children' in asset) {
|
39 | asset.children = asset.children.map(child => replaceStringInAsset(child, source, target));
|
40 | return asset;
|
41 | }
|
42 |
|
43 | throw new Error(
|
44 | `Unknown asset type (${asset.constructor.name})!. ` +
|
45 | 'Unfortunately this type of asset is not supported yet. ' +
|
46 | 'Please raise an issue and we will look into it asap'
|
47 | );
|
48 | }
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | function reHashChunk(chunk, assets, hashFn, nameMap) {
|
57 | const isMainFile = file => file.endsWith('.js') || file.endsWith('.css');
|
58 |
|
59 |
|
60 | chunk.files.filter(isMainFile).forEach((file, index) => {
|
61 | const oldChunkName = chunk.files[index];
|
62 | const asset = assets[oldChunkName];
|
63 | const { fullHash, shortHash: newHash } = hashFn(asset.source());
|
64 |
|
65 | let newChunkName;
|
66 |
|
67 | if (oldChunkName.includes(chunk.renderedHash)) {
|
68 |
|
69 | nameMap[chunk.renderedHash] = newHash;
|
70 | newChunkName = oldChunkName.replace(chunk.renderedHash, newHash);
|
71 |
|
72 |
|
73 | chunk.hash = fullHash;
|
74 | chunk.renderedHash = newHash;
|
75 | } else {
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | const module = Array.from(chunk.modulesIterable).find(m =>
|
88 | oldChunkName.includes(m.renderedHash)
|
89 | );
|
90 |
|
91 |
|
92 |
|
93 | if (!module) return;
|
94 |
|
95 |
|
96 | nameMap[module.renderedHash] = newHash;
|
97 | newChunkName = oldChunkName.replace(module.renderedHash, newHash);
|
98 |
|
99 |
|
100 | module.hash = fullHash;
|
101 | module.renderedHash = newHash;
|
102 | }
|
103 |
|
104 |
|
105 | chunk.files[index] = newChunkName;
|
106 | asset._name = newChunkName;
|
107 | delete assets[oldChunkName];
|
108 | assets[newChunkName] = asset;
|
109 | });
|
110 |
|
111 |
|
112 | chunk.files
|
113 | .filter(file => !isMainFile(file))
|
114 | .forEach(file => {
|
115 | Object.keys(nameMap).forEach(old => {
|
116 | const newHash = nameMap[old];
|
117 | replaceStringInAsset(assets[file], old, newHash);
|
118 | });
|
119 | });
|
120 | }
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 | function replaceOldHashForNewInChunkFiles(chunk, assets, oldHashToNewHashMap) {
|
130 | chunk.files.forEach(file => {
|
131 | Object.keys(oldHashToNewHashMap).forEach(oldHash => {
|
132 | const newHash = oldHashToNewHashMap[oldHash];
|
133 | replaceStringInAsset(assets[file], oldHash, newHash);
|
134 | });
|
135 | });
|
136 | }
|
137 |
|
138 | function sortChunksById(a, b) {
|
139 | if (a.id < b.id) return -1;
|
140 | if (a.id > b.id) return 1;
|
141 | return 0;
|
142 | }
|
143 |
|
144 | OutputHash.prototype.apply = function apply(compiler) {
|
145 | let hashFn;
|
146 |
|
147 | compiler.hooks.emit.tapAsync('OutputHash', (compilation, callback) => {
|
148 | const { outputOptions, chunks, assets } = compilation;
|
149 | const { hashFunction, hashDigest, hashDigestLength, hashSalt } = outputOptions;
|
150 |
|
151 |
|
152 | hashFn = input => {
|
153 | const hashObj = crypto.createHash(hashFunction).update(input);
|
154 | if (hashSalt) hashObj.update(hashSalt);
|
155 | const fullHash = hashObj.digest(hashDigest);
|
156 | return { fullHash, shortHash: fullHash.substr(0, hashDigestLength) };
|
157 | };
|
158 |
|
159 | const nameMap = {};
|
160 | const sortedChunks = chunks.slice().sort((aChunk, bChunk) => {
|
161 | const aEntry = aChunk.hasRuntime();
|
162 | const bEntry = bChunk.hasRuntime();
|
163 | if (aEntry && !bEntry) return 1;
|
164 | if (!aEntry && bEntry) return -1;
|
165 | return sortChunksById(aChunk, bChunk);
|
166 | });
|
167 |
|
168 | sortedChunks.forEach(chunk => {
|
169 | replaceOldHashForNewInChunkFiles(chunk, assets, nameMap);
|
170 | reHashChunk(chunk, assets, hashFn, nameMap);
|
171 | });
|
172 |
|
173 | callback();
|
174 | });
|
175 |
|
176 | if (this.validateOutput) {
|
177 | compiler.hooks.afterEmit.tapAsync('Validate output', (compilation, callback) => {
|
178 | let err;
|
179 | Object.keys(compilation.assets)
|
180 | .filter(assetName => assetName.match(this.validateOutputRegex))
|
181 | .forEach(assetName => {
|
182 | const asset = compilation.assets[assetName];
|
183 | const path = asset.existsAt;
|
184 | const assetContent = fs.readFileSync(path, 'utf8');
|
185 | const { shortHash } = hashFn(assetContent);
|
186 | if (!assetName.includes(shortHash)) {
|
187 | err = new Error(
|
188 | `The hash in ${assetName} does not match the hash of the content (${shortHash})`
|
189 | );
|
190 | }
|
191 | });
|
192 | return callback(err);
|
193 | });
|
194 | }
|
195 | };
|
196 |
|
197 | module.exports = OutputHash;
|