RealContentHashPlugin.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { SyncBailHook } = require("tapable");
  7. const { RawSource, CachedSource, CompatSource } = require("webpack-sources");
  8. const Compilation = require("../Compilation");
  9. const WebpackError = require("../WebpackError");
  10. const { compareSelect, compareStrings } = require("../util/comparators");
  11. const createHash = require("../util/createHash");
  12. /** @typedef {import("webpack-sources").Source} Source */
  13. /** @typedef {import("../Cache").Etag} Etag */
  14. /** @typedef {import("../Compilation").AssetInfo} AssetInfo */
  15. /** @typedef {import("../Compiler")} Compiler */
  16. /** @typedef {typeof import("../util/Hash")} Hash */
  17. const EMPTY_SET = new Set();
  18. /**
  19. * @template T
  20. * @param {T | T[]} itemOrItems item or items
  21. * @param {Set<T>} list list
  22. */
  23. const addToList = (itemOrItems, list) => {
  24. if (Array.isArray(itemOrItems)) {
  25. for (const item of itemOrItems) {
  26. list.add(item);
  27. }
  28. } else if (itemOrItems) {
  29. list.add(itemOrItems);
  30. }
  31. };
  32. /**
  33. * @template T
  34. * @param {T[]} input list
  35. * @param {function(T): Buffer} fn map function
  36. * @returns {Buffer[]} buffers without duplicates
  37. */
  38. const mapAndDeduplicateBuffers = (input, fn) => {
  39. // Buffer.equals compares size first so this should be efficient enough
  40. // If it becomes a performance problem we can use a map and group by size
  41. // instead of looping over all assets.
  42. const result = [];
  43. outer: for (const value of input) {
  44. const buf = fn(value);
  45. for (const other of result) {
  46. if (buf.equals(other)) continue outer;
  47. }
  48. result.push(buf);
  49. }
  50. return result;
  51. };
  52. /**
  53. * Escapes regular expression metacharacters
  54. * @param {string} str String to quote
  55. * @returns {string} Escaped string
  56. */
  57. const quoteMeta = str => {
  58. return str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&");
  59. };
  60. const cachedSourceMap = new WeakMap();
  61. /**
  62. * @param {Source} source source
  63. * @returns {CachedSource} cached source
  64. */
  65. const toCachedSource = source => {
  66. if (source instanceof CachedSource) {
  67. return source;
  68. }
  69. const entry = cachedSourceMap.get(source);
  70. if (entry !== undefined) return entry;
  71. const newSource = new CachedSource(CompatSource.from(source));
  72. cachedSourceMap.set(source, newSource);
  73. return newSource;
  74. };
  75. /** @typedef {Set<string>} OwnHashes */
  76. /** @typedef {Set<string>} ReferencedHashes */
  77. /** @typedef {Set<string>} Hashes */
  78. /**
  79. * @typedef {object} AssetInfoForRealContentHash
  80. * @property {string} name
  81. * @property {AssetInfo} info
  82. * @property {Source} source
  83. * @property {RawSource | undefined} newSource
  84. * @property {RawSource | undefined} newSourceWithoutOwn
  85. * @property {string} content
  86. * @property {OwnHashes | undefined} ownHashes
  87. * @property {Promise<void> | undefined} contentComputePromise
  88. * @property {Promise<void> | undefined} contentComputeWithoutOwnPromise
  89. * @property {ReferencedHashes | undefined} referencedHashes
  90. * @property {Hashes} hashes
  91. */
  92. /**
  93. * @typedef {object} CompilationHooks
  94. * @property {SyncBailHook<[Buffer[], string], string>} updateHash
  95. */
  96. /** @type {WeakMap<Compilation, CompilationHooks>} */
  97. const compilationHooksMap = new WeakMap();
  98. class RealContentHashPlugin {
  99. /**
  100. * @param {Compilation} compilation the compilation
  101. * @returns {CompilationHooks} the attached hooks
  102. */
  103. static getCompilationHooks(compilation) {
  104. if (!(compilation instanceof Compilation)) {
  105. throw new TypeError(
  106. "The 'compilation' argument must be an instance of Compilation"
  107. );
  108. }
  109. let hooks = compilationHooksMap.get(compilation);
  110. if (hooks === undefined) {
  111. hooks = {
  112. updateHash: new SyncBailHook(["content", "oldHash"])
  113. };
  114. compilationHooksMap.set(compilation, hooks);
  115. }
  116. return hooks;
  117. }
  118. /**
  119. * @param {object} options options object
  120. * @param {string | Hash} options.hashFunction the hash function to use
  121. * @param {string} options.hashDigest the hash digest to use
  122. */
  123. constructor({ hashFunction, hashDigest }) {
  124. this._hashFunction = hashFunction;
  125. this._hashDigest = hashDigest;
  126. }
  127. /**
  128. * Apply the plugin
  129. * @param {Compiler} compiler the compiler instance
  130. * @returns {void}
  131. */
  132. apply(compiler) {
  133. compiler.hooks.compilation.tap("RealContentHashPlugin", compilation => {
  134. const cacheAnalyse = compilation.getCache(
  135. "RealContentHashPlugin|analyse"
  136. );
  137. const cacheGenerate = compilation.getCache(
  138. "RealContentHashPlugin|generate"
  139. );
  140. const hooks = RealContentHashPlugin.getCompilationHooks(compilation);
  141. compilation.hooks.processAssets.tapPromise(
  142. {
  143. name: "RealContentHashPlugin",
  144. stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH
  145. },
  146. async () => {
  147. const assets = compilation.getAssets();
  148. /** @type {AssetInfoForRealContentHash[]} */
  149. const assetsWithInfo = [];
  150. /** @type {Map<string, [AssetInfoForRealContentHash]>} */
  151. const hashToAssets = new Map();
  152. for (const { source, info, name } of assets) {
  153. const cachedSource = toCachedSource(source);
  154. const content = /** @type {string} */ (cachedSource.source());
  155. /** @type {Hashes} */
  156. const hashes = new Set();
  157. addToList(info.contenthash, hashes);
  158. /** @type {AssetInfoForRealContentHash} */
  159. const data = {
  160. name,
  161. info,
  162. source: cachedSource,
  163. newSource: undefined,
  164. newSourceWithoutOwn: undefined,
  165. content,
  166. ownHashes: undefined,
  167. contentComputePromise: undefined,
  168. contentComputeWithoutOwnPromise: undefined,
  169. referencedHashes: undefined,
  170. hashes
  171. };
  172. assetsWithInfo.push(data);
  173. for (const hash of hashes) {
  174. const list = hashToAssets.get(hash);
  175. if (list === undefined) {
  176. hashToAssets.set(hash, [data]);
  177. } else {
  178. list.push(data);
  179. }
  180. }
  181. }
  182. if (hashToAssets.size === 0) return;
  183. const hashRegExp = new RegExp(
  184. Array.from(hashToAssets.keys(), quoteMeta).join("|"),
  185. "g"
  186. );
  187. await Promise.all(
  188. assetsWithInfo.map(async asset => {
  189. const { name, source, content, hashes } = asset;
  190. if (Buffer.isBuffer(content)) {
  191. asset.referencedHashes = EMPTY_SET;
  192. asset.ownHashes = EMPTY_SET;
  193. return;
  194. }
  195. const etag = cacheAnalyse.mergeEtags(
  196. cacheAnalyse.getLazyHashedEtag(source),
  197. Array.from(hashes).join("|")
  198. );
  199. [asset.referencedHashes, asset.ownHashes] =
  200. await cacheAnalyse.providePromise(name, etag, () => {
  201. const referencedHashes = new Set();
  202. let ownHashes = new Set();
  203. const inContent = content.match(hashRegExp);
  204. if (inContent) {
  205. for (const hash of inContent) {
  206. if (hashes.has(hash)) {
  207. ownHashes.add(hash);
  208. continue;
  209. }
  210. referencedHashes.add(hash);
  211. }
  212. }
  213. return [referencedHashes, ownHashes];
  214. });
  215. })
  216. );
  217. /**
  218. * @param {string} hash the hash
  219. * @returns {undefined | ReferencedHashes} the referenced hashes
  220. */
  221. const getDependencies = hash => {
  222. const assets = hashToAssets.get(hash);
  223. if (!assets) {
  224. const referencingAssets = assetsWithInfo.filter(asset =>
  225. /** @type {ReferencedHashes} */ (asset.referencedHashes).has(
  226. hash
  227. )
  228. );
  229. const err = new WebpackError(`RealContentHashPlugin
  230. Some kind of unexpected caching problem occurred.
  231. An asset was cached with a reference to another asset (${hash}) that's not in the compilation anymore.
  232. Either the asset was incorrectly cached, or the referenced asset should also be restored from cache.
  233. Referenced by:
  234. ${referencingAssets
  235. .map(a => {
  236. const match = new RegExp(`.{0,20}${quoteMeta(hash)}.{0,20}`).exec(
  237. a.content
  238. );
  239. return ` - ${a.name}: ...${match ? match[0] : "???"}...`;
  240. })
  241. .join("\n")}`);
  242. compilation.errors.push(err);
  243. return undefined;
  244. }
  245. const hashes = new Set();
  246. for (const { referencedHashes, ownHashes } of assets) {
  247. if (!(/** @type {OwnHashes} */ (ownHashes).has(hash))) {
  248. for (const hash of /** @type {OwnHashes} */ (ownHashes)) {
  249. hashes.add(hash);
  250. }
  251. }
  252. for (const hash of /** @type {ReferencedHashes} */ (
  253. referencedHashes
  254. )) {
  255. hashes.add(hash);
  256. }
  257. }
  258. return hashes;
  259. };
  260. /**
  261. * @param {string} hash the hash
  262. * @returns {string} the hash info
  263. */
  264. const hashInfo = hash => {
  265. const assets = hashToAssets.get(hash);
  266. return `${hash} (${Array.from(
  267. /** @type {AssetInfoForRealContentHash[]} */ (assets),
  268. a => a.name
  269. )})`;
  270. };
  271. const hashesInOrder = new Set();
  272. for (const hash of hashToAssets.keys()) {
  273. /**
  274. * @param {string} hash the hash
  275. * @param {Set<string>} stack stack of hashes
  276. */
  277. const add = (hash, stack) => {
  278. const deps = getDependencies(hash);
  279. if (!deps) return;
  280. stack.add(hash);
  281. for (const dep of deps) {
  282. if (hashesInOrder.has(dep)) continue;
  283. if (stack.has(dep)) {
  284. throw new Error(
  285. `Circular hash dependency ${Array.from(
  286. stack,
  287. hashInfo
  288. ).join(" -> ")} -> ${hashInfo(dep)}`
  289. );
  290. }
  291. add(dep, stack);
  292. }
  293. hashesInOrder.add(hash);
  294. stack.delete(hash);
  295. };
  296. if (hashesInOrder.has(hash)) continue;
  297. add(hash, new Set());
  298. }
  299. const hashToNewHash = new Map();
  300. /**
  301. * @param {AssetInfoForRealContentHash} asset asset info
  302. * @returns {Etag} etag
  303. */
  304. const getEtag = asset =>
  305. cacheGenerate.mergeEtags(
  306. cacheGenerate.getLazyHashedEtag(asset.source),
  307. Array.from(
  308. /** @type {ReferencedHashes} */ (asset.referencedHashes),
  309. hash => hashToNewHash.get(hash)
  310. ).join("|")
  311. );
  312. /**
  313. * @param {AssetInfoForRealContentHash} asset asset info
  314. * @returns {Promise<void>}
  315. */
  316. const computeNewContent = asset => {
  317. if (asset.contentComputePromise) return asset.contentComputePromise;
  318. return (asset.contentComputePromise = (async () => {
  319. if (
  320. /** @type {OwnHashes} */ (asset.ownHashes).size > 0 ||
  321. Array.from(
  322. /** @type {ReferencedHashes} */
  323. (asset.referencedHashes)
  324. ).some(hash => hashToNewHash.get(hash) !== hash)
  325. ) {
  326. const identifier = asset.name;
  327. const etag = getEtag(asset);
  328. asset.newSource = await cacheGenerate.providePromise(
  329. identifier,
  330. etag,
  331. () => {
  332. const newContent = asset.content.replace(hashRegExp, hash =>
  333. hashToNewHash.get(hash)
  334. );
  335. return new RawSource(newContent);
  336. }
  337. );
  338. }
  339. })());
  340. };
  341. /**
  342. * @param {AssetInfoForRealContentHash} asset asset info
  343. * @returns {Promise<void>}
  344. */
  345. const computeNewContentWithoutOwn = asset => {
  346. if (asset.contentComputeWithoutOwnPromise)
  347. return asset.contentComputeWithoutOwnPromise;
  348. return (asset.contentComputeWithoutOwnPromise = (async () => {
  349. if (
  350. /** @type {OwnHashes} */ (asset.ownHashes).size > 0 ||
  351. Array.from(
  352. /** @type {ReferencedHashes} */
  353. (asset.referencedHashes)
  354. ).some(hash => hashToNewHash.get(hash) !== hash)
  355. ) {
  356. const identifier = asset.name + "|without-own";
  357. const etag = getEtag(asset);
  358. asset.newSourceWithoutOwn = await cacheGenerate.providePromise(
  359. identifier,
  360. etag,
  361. () => {
  362. const newContent = asset.content.replace(
  363. hashRegExp,
  364. hash => {
  365. if (
  366. /** @type {OwnHashes} */ (asset.ownHashes).has(hash)
  367. ) {
  368. return "";
  369. }
  370. return hashToNewHash.get(hash);
  371. }
  372. );
  373. return new RawSource(newContent);
  374. }
  375. );
  376. }
  377. })());
  378. };
  379. const comparator = compareSelect(a => a.name, compareStrings);
  380. for (const oldHash of hashesInOrder) {
  381. const assets =
  382. /** @type {AssetInfoForRealContentHash[]} */
  383. (hashToAssets.get(oldHash));
  384. assets.sort(comparator);
  385. await Promise.all(
  386. assets.map(asset =>
  387. /** @type {OwnHashes} */ (asset.ownHashes).has(oldHash)
  388. ? computeNewContentWithoutOwn(asset)
  389. : computeNewContent(asset)
  390. )
  391. );
  392. const assetsContent = mapAndDeduplicateBuffers(assets, asset => {
  393. if (/** @type {OwnHashes} */ (asset.ownHashes).has(oldHash)) {
  394. return asset.newSourceWithoutOwn
  395. ? asset.newSourceWithoutOwn.buffer()
  396. : asset.source.buffer();
  397. } else {
  398. return asset.newSource
  399. ? asset.newSource.buffer()
  400. : asset.source.buffer();
  401. }
  402. });
  403. let newHash = hooks.updateHash.call(assetsContent, oldHash);
  404. if (!newHash) {
  405. const hash = createHash(this._hashFunction);
  406. if (compilation.outputOptions.hashSalt) {
  407. hash.update(compilation.outputOptions.hashSalt);
  408. }
  409. for (const content of assetsContent) {
  410. hash.update(content);
  411. }
  412. const digest = hash.digest(this._hashDigest);
  413. newHash = /** @type {string} */ (digest.slice(0, oldHash.length));
  414. }
  415. hashToNewHash.set(oldHash, newHash);
  416. }
  417. await Promise.all(
  418. assetsWithInfo.map(async asset => {
  419. await computeNewContent(asset);
  420. const newName = asset.name.replace(hashRegExp, hash =>
  421. hashToNewHash.get(hash)
  422. );
  423. const infoUpdate = {};
  424. const hash = asset.info.contenthash;
  425. infoUpdate.contenthash = Array.isArray(hash)
  426. ? hash.map(hash => hashToNewHash.get(hash))
  427. : hashToNewHash.get(hash);
  428. if (asset.newSource !== undefined) {
  429. compilation.updateAsset(
  430. asset.name,
  431. asset.newSource,
  432. infoUpdate
  433. );
  434. } else {
  435. compilation.updateAsset(asset.name, asset.source, infoUpdate);
  436. }
  437. if (asset.name !== newName) {
  438. compilation.renameAsset(asset.name, newName);
  439. }
  440. })
  441. );
  442. }
  443. );
  444. });
  445. }
  446. }
  447. module.exports = RealContentHashPlugin;