UNPKG

7.1 kBJavaScriptView Raw
1const crypto = require('crypto');
2const fs = require('fs');
3
4function OutputHash({ validateOutput = false, validateOutputRegex = /^.*$/ } = {}) {
5 this.validateOutput = validateOutput;
6 this.validateOutputRegex = validateOutputRegex;
7}
8
9/**
10 * Replaces a string in an asset
11 */
12function 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 // ReplaceSource
20 if ('_source' in asset) {
21 asset._source = replaceStringInAsset(asset._source, source, target);
22 return asset;
23 }
24
25 // CachedSource
26 if ('_cachedSource' in asset) {
27 asset._cachedSource = asset.source().replace(sourceRE, target);
28 return asset;
29 }
30
31 // RawSource / SourceMapSource
32 if ('_value' in asset) {
33 asset._value = asset.source().replace(sourceRE, target);
34 return asset;
35 }
36
37 // ConcatSource
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 * Computes the new hash of a chunk.
52 *
53 * This function updates the *name* of the main file (i.e. source code), and the *content* of the
54 * secondary files (i.e source maps)
55 */
56function reHashChunk(chunk, assets, hashFn, nameMap) {
57 const isMainFile = file => file.endsWith('.js') || file.endsWith('.css');
58
59 // Update the name of the main files
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 // Save the hash map for replacing the secondary files
69 nameMap[chunk.renderedHash] = newHash;
70 newChunkName = oldChunkName.replace(chunk.renderedHash, newHash);
71
72 // Keep the chunk hashes in sync
73 chunk.hash = fullHash;
74 chunk.renderedHash = newHash;
75 } else {
76 // This is a massive hack:
77 //
78 // The oldHash of the main file is in `chunk.renderedHash`. But some plugins add a
79 // second "main" file to the chunk (for example, `mini-css-extract-plugin` adds a
80 // css file). That other main file has to be rehashed too, but we don't know the
81 // oldHash of the file, so we don't know what string we have to replace by the new
82 // hash.
83 //
84 // However, the hash present in the file name must be one of the hashes of the
85 // modules inside the chunk (modules[].renderedHash). So we try to replace each
86 // module hash with the new hash.
87 const module = Array.from(chunk.modulesIterable).find(m =>
88 oldChunkName.includes(m.renderedHash)
89 );
90
91 // Can't find a module with this hash... not sure what is going on, just return and
92 // hope for the best.
93 if (!module) return;
94
95 // Save the hash map for replacing the secondary files
96 nameMap[module.renderedHash] = newHash;
97 newChunkName = oldChunkName.replace(module.renderedHash, newHash);
98
99 // Keep the module hashes in sync
100 module.hash = fullHash;
101 module.renderedHash = newHash;
102 }
103
104 // Change file name to include the new hash
105 chunk.files[index] = newChunkName;
106 asset._name = newChunkName;
107 delete assets[oldChunkName];
108 assets[newChunkName] = asset;
109 });
110
111 // Update the content of the rest of the files in the chunk
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 * Replaces old hashes for new hashes in chunk files.
124 *
125 * This function iterates through file contents and replaces all the ocurrences of old hashes
126 * for new ones. We assume hashes are unique enough, so that we don't accidentally hit a
127 * collision and replace existing data.
128 */
129function 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
138function sortChunksById(a, b) {
139 if (a.id < b.id) return -1;
140 if (a.id > b.id) return 1;
141 return 0;
142}
143
144OutputHash.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 // Reuses webpack options
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
197module.exports = OutputHash;