RealContentHashPlugin.js 15 KB

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