| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | /* | 
					
						
							|  |  |  | 	MIT License http://www.opensource.org/licenses/mit-license.php
 | 
					
						
							|  |  |  | 	Author Tobias Koppers @sokra | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | "use strict"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-20 15:54:02 +08:00
										 |  |  | const { RawSource, CachedSource, CompatSource } = require("webpack-sources"); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | const Compilation = require("../Compilation"); | 
					
						
							|  |  |  | const { compareSelect, compareStrings } = require("../util/comparators"); | 
					
						
							|  |  |  | const createHash = require("../util/createHash"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** @typedef {import("../Compiler")} Compiler */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | const EMPTY_SET = new Set(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | const addToList = (itemOrItems, list) => { | 
					
						
							|  |  |  | 	if (Array.isArray(itemOrItems)) { | 
					
						
							|  |  |  | 		for (const item of itemOrItems) { | 
					
						
							|  |  |  | 			list.add(item); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} else if (itemOrItems) { | 
					
						
							|  |  |  | 		list.add(itemOrItems); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Escapes regular expression metacharacters | 
					
						
							|  |  |  |  * @param {string} str String to quote | 
					
						
							|  |  |  |  * @returns {string} Escaped string | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const quoteMeta = str => { | 
					
						
							|  |  |  | 	return str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&"); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-20 15:54:02 +08:00
										 |  |  | const cachedSourceMap = new WeakMap(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const toCachedSource = source => { | 
					
						
							|  |  |  | 	if (source instanceof CachedSource) { | 
					
						
							|  |  |  | 		return source; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	const entry = cachedSourceMap.get(source); | 
					
						
							|  |  |  | 	if (entry !== undefined) return entry; | 
					
						
							|  |  |  | 	const newSource = new CachedSource(CompatSource.from(source)); | 
					
						
							|  |  |  | 	cachedSourceMap.set(source, newSource); | 
					
						
							|  |  |  | 	return newSource; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | class RealContentHashPlugin { | 
					
						
							|  |  |  | 	constructor({ hashFunction, hashDigest }) { | 
					
						
							|  |  |  | 		this._hashFunction = hashFunction; | 
					
						
							|  |  |  | 		this._hashDigest = hashDigest; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Apply the plugin | 
					
						
							|  |  |  | 	 * @param {Compiler} compiler the compiler instance | 
					
						
							|  |  |  | 	 * @returns {void} | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	apply(compiler) { | 
					
						
							|  |  |  | 		compiler.hooks.compilation.tap("RealContentHashPlugin", compilation => { | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 			const cacheAnalyse = compilation.getCache( | 
					
						
							|  |  |  | 				"RealContentHashPlugin|analyse" | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 			const cacheGenerate = compilation.getCache( | 
					
						
							|  |  |  | 				"RealContentHashPlugin|generate" | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 			compilation.hooks.processAssets.tapPromise( | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 				{ | 
					
						
							|  |  |  | 					name: "RealContentHashPlugin", | 
					
						
							|  |  |  | 					stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH | 
					
						
							|  |  |  | 				}, | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 				async () => { | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 					const assets = compilation.getAssets(); | 
					
						
							|  |  |  | 					const assetsWithInfo = []; | 
					
						
							|  |  |  | 					const hashToAssets = new Map(); | 
					
						
							|  |  |  | 					for (const { source, info, name } of assets) { | 
					
						
							| 
									
										
										
										
											2020-08-20 15:54:02 +08:00
										 |  |  | 						const cachedSource = toCachedSource(source); | 
					
						
							|  |  |  | 						const content = cachedSource.source(); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 						/** @type {Set<string>} */ | 
					
						
							|  |  |  | 						const hashes = new Set(); | 
					
						
							|  |  |  | 						addToList(info.contenthash, hashes); | 
					
						
							|  |  |  | 						const data = { | 
					
						
							|  |  |  | 							name, | 
					
						
							|  |  |  | 							info, | 
					
						
							| 
									
										
										
										
											2020-08-20 15:54:02 +08:00
										 |  |  | 							source: cachedSource, | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 							/** @type {RawSource | undefined} */ | 
					
						
							| 
									
										
										
										
											2020-08-19 15:46:41 +08:00
										 |  |  | 							newSource: undefined, | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 							content, | 
					
						
							|  |  |  | 							hasOwnHash: false, | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 							contentComputePromise: false, | 
					
						
							|  |  |  | 							/** @type {Set<string>} */ | 
					
						
							|  |  |  | 							referencedHashes: undefined, | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 							hashes | 
					
						
							|  |  |  | 						}; | 
					
						
							|  |  |  | 						assetsWithInfo.push(data); | 
					
						
							|  |  |  | 						for (const hash of hashes) { | 
					
						
							|  |  |  | 							const list = hashToAssets.get(hash); | 
					
						
							|  |  |  | 							if (list === undefined) { | 
					
						
							|  |  |  | 								hashToAssets.set(hash, [data]); | 
					
						
							|  |  |  | 							} else { | 
					
						
							|  |  |  | 								list.push(data); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2020-08-19 17:37:17 +08:00
										 |  |  | 					if (hashToAssets.size === 0) return; | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 					const hashRegExp = new RegExp( | 
					
						
							|  |  |  | 						Array.from(hashToAssets.keys(), quoteMeta).join("|"), | 
					
						
							|  |  |  | 						"g" | 
					
						
							|  |  |  | 					); | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 					await Promise.all( | 
					
						
							|  |  |  | 						assetsWithInfo.map(async asset => { | 
					
						
							|  |  |  | 							const { name, source, content, hashes } = asset; | 
					
						
							|  |  |  | 							if (Buffer.isBuffer(content)) { | 
					
						
							|  |  |  | 								asset.referencedHashes = EMPTY_SET; | 
					
						
							|  |  |  | 								return; | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 							} | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 							const etag = cacheAnalyse.mergeEtags( | 
					
						
							|  |  |  | 								cacheAnalyse.getLazyHashedEtag(source), | 
					
						
							|  |  |  | 								Array.from(hashes).join("|") | 
					
						
							|  |  |  | 							); | 
					
						
							| 
									
										
										
										
											2020-09-16 17:12:41 +08:00
										 |  |  | 							[ | 
					
						
							|  |  |  | 								asset.referencedHashes, | 
					
						
							|  |  |  | 								asset.hasOwnHash | 
					
						
							|  |  |  | 							] = await cacheAnalyse.providePromise(name, etag, () => { | 
					
						
							|  |  |  | 								const referencedHashes = new Set(); | 
					
						
							|  |  |  | 								let hasOwnHash = false; | 
					
						
							|  |  |  | 								const inContent = content.match(hashRegExp); | 
					
						
							|  |  |  | 								if (inContent) { | 
					
						
							|  |  |  | 									for (const hash of inContent) { | 
					
						
							|  |  |  | 										if (hashes.has(hash)) { | 
					
						
							|  |  |  | 											hasOwnHash = true; | 
					
						
							|  |  |  | 											continue; | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 										} | 
					
						
							| 
									
										
										
										
											2020-09-16 17:12:41 +08:00
										 |  |  | 										referencedHashes.add(hash); | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 									} | 
					
						
							|  |  |  | 								} | 
					
						
							| 
									
										
										
										
											2020-09-16 17:12:41 +08:00
										 |  |  | 								return [referencedHashes, hasOwnHash]; | 
					
						
							|  |  |  | 							}); | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 						}) | 
					
						
							|  |  |  | 					); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 					const getDependencies = hash => { | 
					
						
							|  |  |  | 						const assets = hashToAssets.get(hash); | 
					
						
							|  |  |  | 						const hashes = new Set(); | 
					
						
							|  |  |  | 						for (const { referencedHashes } of assets) { | 
					
						
							|  |  |  | 							for (const hash of referencedHashes) { | 
					
						
							|  |  |  | 								hashes.add(hash); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 						return hashes; | 
					
						
							|  |  |  | 					}; | 
					
						
							|  |  |  | 					const hashInfo = hash => { | 
					
						
							|  |  |  | 						const assets = hashToAssets.get(hash); | 
					
						
							|  |  |  | 						return `${hash} (${Array.from(assets, a => a.name)})`; | 
					
						
							|  |  |  | 					}; | 
					
						
							|  |  |  | 					const hashesInOrder = new Set(); | 
					
						
							|  |  |  | 					for (const hash of hashToAssets.keys()) { | 
					
						
							|  |  |  | 						const add = (hash, stack) => { | 
					
						
							|  |  |  | 							const deps = getDependencies(hash); | 
					
						
							|  |  |  | 							stack.add(hash); | 
					
						
							|  |  |  | 							for (const dep of deps) { | 
					
						
							|  |  |  | 								if (hashesInOrder.has(dep)) continue; | 
					
						
							|  |  |  | 								if (stack.has(dep)) { | 
					
						
							|  |  |  | 									throw new Error( | 
					
						
							|  |  |  | 										`Circular hash dependency ${Array.from( | 
					
						
							|  |  |  | 											stack, | 
					
						
							|  |  |  | 											hashInfo | 
					
						
							|  |  |  | 										).join(" -> ")} -> ${hashInfo(dep)}`
 | 
					
						
							|  |  |  | 									); | 
					
						
							|  |  |  | 								} | 
					
						
							|  |  |  | 								add(dep, stack); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 							hashesInOrder.add(hash); | 
					
						
							|  |  |  | 							stack.delete(hash); | 
					
						
							|  |  |  | 						}; | 
					
						
							|  |  |  | 						if (hashesInOrder.has(hash)) continue; | 
					
						
							|  |  |  | 						add(hash, new Set()); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					const hashToNewHash = new Map(); | 
					
						
							|  |  |  | 					const computeNewContent = (asset, includeOwn) => { | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 						if (asset.contentComputePromise) return asset.contentComputePromise; | 
					
						
							|  |  |  | 						return (asset.contentComputePromise = (async () => { | 
					
						
							|  |  |  | 							if ( | 
					
						
							|  |  |  | 								asset.hasOwnHash || | 
					
						
							|  |  |  | 								Array.from(asset.referencedHashes).some( | 
					
						
							|  |  |  | 									hash => hashToNewHash.get(hash) !== hash | 
					
						
							|  |  |  | 								) | 
					
						
							|  |  |  | 							) { | 
					
						
							|  |  |  | 								const identifier = | 
					
						
							|  |  |  | 									asset.name + | 
					
						
							|  |  |  | 									(includeOwn && asset.hasOwnHash ? "|with-own" : ""); | 
					
						
							|  |  |  | 								const etag = cacheGenerate.mergeEtags( | 
					
						
							|  |  |  | 									cacheGenerate.getLazyHashedEtag(asset.source), | 
					
						
							|  |  |  | 									Array.from(asset.referencedHashes, hash => | 
					
						
							|  |  |  | 										hashToNewHash.get(hash) | 
					
						
							|  |  |  | 									).join("|") | 
					
						
							|  |  |  | 								); | 
					
						
							|  |  |  | 								asset.newSource = await cacheGenerate.providePromise( | 
					
						
							|  |  |  | 									identifier, | 
					
						
							|  |  |  | 									etag, | 
					
						
							|  |  |  | 									() => { | 
					
						
							|  |  |  | 										const newContent = asset.content.replace( | 
					
						
							|  |  |  | 											hashRegExp, | 
					
						
							|  |  |  | 											hash => { | 
					
						
							|  |  |  | 												if (!includeOwn && asset.hashes.has(hash)) { | 
					
						
							|  |  |  | 													return ""; | 
					
						
							|  |  |  | 												} | 
					
						
							|  |  |  | 												return hashToNewHash.get(hash); | 
					
						
							|  |  |  | 											} | 
					
						
							|  |  |  | 										); | 
					
						
							|  |  |  | 										return new RawSource(newContent); | 
					
						
							|  |  |  | 									} | 
					
						
							|  |  |  | 								); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						})()); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 					}; | 
					
						
							|  |  |  | 					const comparator = compareSelect(a => a.name, compareStrings); | 
					
						
							|  |  |  | 					for (const oldHash of hashesInOrder) { | 
					
						
							|  |  |  | 						const assets = hashToAssets.get(oldHash); | 
					
						
							|  |  |  | 						assets.sort(comparator); | 
					
						
							|  |  |  | 						const hash = createHash(this._hashFunction); | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 						await Promise.all(assets.map(computeNewContent)); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 						for (const asset of assets) { | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 							hash.update( | 
					
						
							|  |  |  | 								asset.newSource | 
					
						
							|  |  |  | 									? asset.newSource.buffer() | 
					
						
							|  |  |  | 									: asset.source.buffer() | 
					
						
							|  |  |  | 							); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 						} | 
					
						
							|  |  |  | 						const digest = hash.digest(this._hashDigest); | 
					
						
							|  |  |  | 						const newHash = digest.slice(0, oldHash.length); | 
					
						
							| 
									
										
										
										
											2020-08-19 15:46:41 +08:00
										 |  |  | 						hashToNewHash.set(oldHash, newHash); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 					await Promise.all( | 
					
						
							|  |  |  | 						assetsWithInfo.map(async asset => { | 
					
						
							|  |  |  | 							// recomputed content with it's own hash
 | 
					
						
							|  |  |  | 							if (asset.hasOwnHash) { | 
					
						
							|  |  |  | 								asset.contentComputePromise = undefined; | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 							await computeNewContent(asset, true); | 
					
						
							|  |  |  | 							const newName = asset.name.replace(hashRegExp, hash => | 
					
						
							|  |  |  | 								hashToNewHash.get(hash) | 
					
						
							|  |  |  | 							); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 							const infoUpdate = {}; | 
					
						
							|  |  |  | 							const hash = asset.info.contenthash; | 
					
						
							|  |  |  | 							infoUpdate.contenthash = Array.isArray(hash) | 
					
						
							|  |  |  | 								? hash.map(hash => hashToNewHash.get(hash)) | 
					
						
							|  |  |  | 								: hashToNewHash.get(hash); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 							if (asset.newSource !== undefined) { | 
					
						
							|  |  |  | 								compilation.updateAsset( | 
					
						
							|  |  |  | 									asset.name, | 
					
						
							|  |  |  | 									asset.newSource, | 
					
						
							|  |  |  | 									infoUpdate | 
					
						
							|  |  |  | 								); | 
					
						
							|  |  |  | 							} else { | 
					
						
							|  |  |  | 								compilation.updateAsset(asset.name, asset.source, infoUpdate); | 
					
						
							|  |  |  | 							} | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-19 17:25:53 +08:00
										 |  |  | 							if (asset.name !== newName) { | 
					
						
							|  |  |  | 								compilation.renameAsset(asset.name, newName); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						}) | 
					
						
							|  |  |  | 					); | 
					
						
							| 
									
										
										
										
											2020-08-19 03:14:43 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			); | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = RealContentHashPlugin; |