RealContentHashPlugin.js 15 KB

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