source: frontend/node_modules/webpack/lib/optimize/RealContentHashPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 17.2 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const { SyncBailHook } = require("tapable");
9const { CachedSource, CompatSource, RawSource } = require("webpack-sources");
10const Compilation = require("../Compilation");
11const WebpackError = require("../errors/WebpackError");
12const { compareSelect, compareStrings } = require("../util/comparators");
13const createHash = require("../util/createHash");
14
15/** @typedef {import("../../declarations/WebpackOptions").HashFunction} HashFunction */
16/** @typedef {import("../../declarations/WebpackOptions").HashDigest} HashDigest */
17/** @typedef {import("webpack-sources").Source} Source */
18/** @typedef {import("../Cache").Etag} Etag */
19/** @typedef {import("../Compilation").AssetInfo} AssetInfo */
20/** @typedef {import("../Compiler")} Compiler */
21/** @typedef {typeof import("../util/Hash")} Hash */
22
23/**
24 * Defines the comparator type used by this module.
25 * @template T
26 * @typedef {import("../util/comparators").Comparator<T>} Comparator
27 */
28
29/** @type {Hashes} */
30const EMPTY_SET = new Set();
31
32/**
33 * Adds the provided item or item to this object.
34 * @template T
35 * @param {T | T[]} itemOrItems item or items
36 * @param {Set<T>} list list
37 */
38const addToList = (itemOrItems, list) => {
39 if (Array.isArray(itemOrItems)) {
40 for (const item of itemOrItems) {
41 list.add(item);
42 }
43 } else if (itemOrItems) {
44 list.add(itemOrItems);
45 }
46};
47
48/**
49 * Compares two non-empty buffer chunk arrays for byte-equality without
50 * allocating a concatenated buffer.
51 * @param {Buffer[]} a first chunk array
52 * @param {Buffer[]} b second chunk array
53 * @returns {boolean} true if the concatenations are byte-equal
54 */
55const bufferArraysEqual = (a, b) => {
56 let aIdx = 0;
57 let aOff = 0;
58 let bIdx = 0;
59 let bOff = 0;
60 while (aIdx < a.length && bIdx < b.length) {
61 const aBuf = a[aIdx];
62 const bBuf = b[bIdx];
63 const len = Math.min(aBuf.length - aOff, bBuf.length - bOff);
64 if (aBuf.compare(bBuf, bOff, bOff + len, aOff, aOff + len) !== 0) {
65 return false;
66 }
67 aOff += len;
68 bOff += len;
69 if (aOff === aBuf.length) {
70 aIdx++;
71 aOff = 0;
72 }
73 if (bOff === bBuf.length) {
74 bIdx++;
75 bOff = 0;
76 }
77 }
78 return aIdx === a.length && bIdx === b.length;
79};
80
81/**
82 * Map sources to their buffer chunks and deduplicate by total byte content,
83 * grouping by total length first to avoid full comparisons.
84 * @template T
85 * @param {T[]} input list
86 * @param {(item: T) => Source} fn map function returning a Source
87 * @returns {Buffer[][]} unique chunk arrays
88 */
89const mapAndDeduplicateSourceBuffers = (input, fn) => {
90 /** @type {Map<number, Buffer[][]>} */
91 const bySize = new Map();
92 /** @type {Buffer[][]} */
93 const result = [];
94 for (const value of input) {
95 const source = fn(value);
96 // TODO webpack 6: drop the `buffers` check, require webpack-sources >= 3.4
97 // and call `source.buffers()` unconditionally.
98 const chunks =
99 // TODO remove in webpack 6, this is protection against authors who directly use `webpack-sources` outdated version
100 typeof source.buffers === "function"
101 ? source.buffers()
102 : [source.buffer()];
103 let total = 0;
104 for (const c of chunks) total += c.length;
105 const sameSize = bySize.get(total);
106 if (sameSize) {
107 let duplicate = false;
108 for (const other of sameSize) {
109 if (bufferArraysEqual(chunks, other)) {
110 duplicate = true;
111 break;
112 }
113 }
114 if (duplicate) continue;
115 sameSize.push(chunks);
116 } else {
117 bySize.set(total, [chunks]);
118 }
119 result.push(chunks);
120 }
121 return result;
122};
123
124/**
125 * Escapes regular expression metacharacters
126 * @param {string} str String to quote
127 * @returns {string} Escaped string
128 */
129const quoteMeta = (str) => str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&");
130
131/** @type {WeakMap<Source, CachedSource>} */
132const cachedSourceMap = new WeakMap();
133
134/**
135 * Returns cached source.
136 * @param {Source} source source
137 * @returns {CachedSource} cached source
138 */
139const toCachedSource = (source) => {
140 if (source instanceof CachedSource) {
141 return source;
142 }
143 const entry = cachedSourceMap.get(source);
144 if (entry !== undefined) return entry;
145 const newSource = new CachedSource(CompatSource.from(source));
146 cachedSourceMap.set(source, newSource);
147 return newSource;
148};
149
150/** @typedef {Set<string>} Hashes */
151
152/**
153 * Defines the asset info for real content hash type used by this module.
154 * @typedef {object} AssetInfoForRealContentHash
155 * @property {string} name
156 * @property {AssetInfo} info
157 * @property {Source} source
158 * @property {RawSource | undefined} newSource
159 * @property {RawSource | undefined} newSourceWithoutOwn
160 * @property {string} content
161 * @property {Hashes | undefined} ownHashes
162 * @property {Promise<void> | undefined} contentComputePromise
163 * @property {Promise<void> | undefined} contentComputeWithoutOwnPromise
164 * @property {Hashes | undefined} referencedHashes
165 * @property {Hashes} hashes
166 */
167
168/**
169 * Defines the compilation hooks type used by this module.
170 * @typedef {object} CompilationHooks
171 * @property {SyncBailHook<[Buffer[], string], string | void>} updateHash
172 */
173
174/** @type {WeakMap<Compilation, CompilationHooks>} */
175const compilationHooksMap = new WeakMap();
176
177/**
178 * Defines the real content hash plugin options type used by this module.
179 * @typedef {object} RealContentHashPluginOptions
180 * @property {HashFunction} hashFunction the hash function to use
181 * @property {HashDigest} hashDigest the hash digest to use
182 */
183
184const PLUGIN_NAME = "RealContentHashPlugin";
185
186class RealContentHashPlugin {
187 /**
188 * Returns the attached hooks.
189 * @param {Compilation} compilation the compilation
190 * @returns {CompilationHooks} the attached hooks
191 */
192 static getCompilationHooks(compilation) {
193 if (!(compilation instanceof Compilation)) {
194 throw new TypeError(
195 "The 'compilation' argument must be an instance of Compilation"
196 );
197 }
198 let hooks = compilationHooksMap.get(compilation);
199 if (hooks === undefined) {
200 hooks = {
201 updateHash: new SyncBailHook(["content", "oldHash"])
202 };
203 compilationHooksMap.set(compilation, hooks);
204 }
205 return hooks;
206 }
207
208 /**
209 * Creates an instance of RealContentHashPlugin.
210 * @param {RealContentHashPluginOptions} options options
211 */
212 constructor({ hashFunction, hashDigest }) {
213 /** @type {HashFunction} */
214 this._hashFunction = hashFunction;
215 /** @type {HashDigest} */
216 this._hashDigest = hashDigest;
217 }
218
219 /**
220 * Applies the plugin by registering its hooks on the compiler.
221 * @param {Compiler} compiler the compiler instance
222 * @returns {void}
223 */
224 apply(compiler) {
225 compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
226 const cacheAnalyse = compilation.getCache(
227 "RealContentHashPlugin|analyse"
228 );
229 const cacheGenerate = compilation.getCache(
230 "RealContentHashPlugin|generate"
231 );
232 const hooks = RealContentHashPlugin.getCompilationHooks(compilation);
233 compilation.hooks.processAssets.tapPromise(
234 {
235 name: PLUGIN_NAME,
236 stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH
237 },
238 async () => {
239 const assets = compilation.getAssets();
240 /** @type {AssetInfoForRealContentHash[]} */
241 const assetsWithInfo = [];
242 /** @type {Map<string, [AssetInfoForRealContentHash]>} */
243 const hashToAssets = new Map();
244 for (const { source, info, name } of assets) {
245 const cachedSource = toCachedSource(source);
246 const content = /** @type {string} */ (cachedSource.source());
247 /** @type {Hashes} */
248 const hashes = new Set();
249 addToList(info.contenthash, hashes);
250 /** @type {AssetInfoForRealContentHash} */
251 const data = {
252 name,
253 info,
254 source: cachedSource,
255 newSource: undefined,
256 newSourceWithoutOwn: undefined,
257 content,
258 ownHashes: undefined,
259 contentComputePromise: undefined,
260 contentComputeWithoutOwnPromise: undefined,
261 referencedHashes: undefined,
262 hashes
263 };
264 assetsWithInfo.push(data);
265 for (const hash of hashes) {
266 const list = hashToAssets.get(hash);
267 if (list === undefined) {
268 hashToAssets.set(hash, [data]);
269 } else {
270 list.push(data);
271 }
272 }
273 }
274 if (hashToAssets.size === 0) return;
275 const hashRegExp = new RegExp(
276 Array.from(hashToAssets.keys(), quoteMeta).join("|"),
277 "g"
278 );
279 await Promise.all(
280 assetsWithInfo.map(async (asset) => {
281 const { name, source, content, hashes } = asset;
282 if (Buffer.isBuffer(content)) {
283 asset.referencedHashes = EMPTY_SET;
284 asset.ownHashes = EMPTY_SET;
285 return;
286 }
287 const etag = cacheAnalyse.mergeEtags(
288 cacheAnalyse.getLazyHashedEtag(source),
289 [...hashes].join("|")
290 );
291 [asset.referencedHashes, asset.ownHashes] =
292 await cacheAnalyse.providePromise(name, etag, () => {
293 /** @type {Hashes} */
294 const referencedHashes = new Set();
295 /** @type {Hashes} */
296 const ownHashes = new Set();
297 const inContent = content.match(hashRegExp);
298 if (inContent) {
299 for (const hash of inContent) {
300 if (hashes.has(hash)) {
301 ownHashes.add(hash);
302 continue;
303 }
304 referencedHashes.add(hash);
305 }
306 }
307 return [referencedHashes, ownHashes];
308 });
309 })
310 );
311 /**
312 * Returns the referenced hashes.
313 * @param {string} hash the hash
314 * @returns {undefined | Hashes} the referenced hashes
315 */
316 const getDependencies = (hash) => {
317 const assets = hashToAssets.get(hash);
318 if (!assets) {
319 const referencingAssets = assetsWithInfo.filter((asset) =>
320 /** @type {Hashes} */ (asset.referencedHashes).has(hash)
321 );
322 const err = new WebpackError(`RealContentHashPlugin
323Some kind of unexpected caching problem occurred.
324An asset was cached with a reference to another asset (${hash}) that's not in the compilation anymore.
325Either the asset was incorrectly cached, or the referenced asset should also be restored from cache.
326Referenced by:
327${referencingAssets
328 .map((a) => {
329 const match = new RegExp(`.{0,20}${quoteMeta(hash)}.{0,20}`).exec(
330 a.content
331 );
332 return ` - ${a.name}: ...${match ? match[0] : "???"}...`;
333 })
334 .join("\n")}`);
335 compilation.errors.push(err);
336 return;
337 }
338 /** @type {Hashes} */
339 const hashes = new Set();
340 for (const { referencedHashes, ownHashes } of assets) {
341 if (!(/** @type {Hashes} */ (ownHashes).has(hash))) {
342 for (const hash of /** @type {Hashes} */ (ownHashes)) {
343 hashes.add(hash);
344 }
345 }
346 for (const hash of /** @type {Hashes} */ (referencedHashes)) {
347 hashes.add(hash);
348 }
349 }
350 return hashes;
351 };
352 /**
353 * Returns the hash info.
354 * @param {string} hash the hash
355 * @returns {string} the hash info
356 */
357 const hashInfo = (hash) => {
358 const assets = hashToAssets.get(hash);
359 return `${hash} (${Array.from(
360 /** @type {AssetInfoForRealContentHash[]} */ (assets),
361 (a) => a.name
362 )})`;
363 };
364 /** @type {Hashes} */
365 const hashesInOrder = new Set();
366 for (const hash of hashToAssets.keys()) {
367 /**
368 * Processes the provided hash.
369 * @param {string} hash the hash
370 * @param {Set<string>} stack stack of hashes
371 */
372 const add = (hash, stack) => {
373 const deps = getDependencies(hash);
374 if (!deps) return;
375 stack.add(hash);
376 for (const dep of deps) {
377 if (hashesInOrder.has(dep)) continue;
378 if (stack.has(dep)) {
379 throw new Error(
380 `Circular hash dependency ${Array.from(
381 stack,
382 hashInfo
383 ).join(" -> ")} -> ${hashInfo(dep)}`
384 );
385 }
386 add(dep, stack);
387 }
388 hashesInOrder.add(hash);
389 stack.delete(hash);
390 };
391 if (hashesInOrder.has(hash)) continue;
392 add(hash, new Set());
393 }
394 /** @type {Map<string, string>} */
395 const hashToNewHash = new Map();
396 /**
397 * Returns etag.
398 * @param {AssetInfoForRealContentHash} asset asset info
399 * @returns {Etag} etag
400 */
401 const getEtag = (asset) =>
402 cacheGenerate.mergeEtags(
403 cacheGenerate.getLazyHashedEtag(asset.source),
404 Array.from(
405 /** @type {Hashes} */ (asset.referencedHashes),
406 (hash) => hashToNewHash.get(hash)
407 ).join("|")
408 );
409 /**
410 * Compute new content.
411 * @param {AssetInfoForRealContentHash} asset asset info
412 * @returns {Promise<void>}
413 */
414 const computeNewContent = (asset) => {
415 if (asset.contentComputePromise) return asset.contentComputePromise;
416 return (asset.contentComputePromise = (async () => {
417 if (
418 /** @type {Hashes} */ (asset.ownHashes).size > 0 ||
419 [.../** @type {Hashes} */ (asset.referencedHashes)].some(
420 (hash) => hashToNewHash.get(hash) !== hash
421 )
422 ) {
423 const identifier = asset.name;
424 const etag = getEtag(asset);
425 asset.newSource = await cacheGenerate.providePromise(
426 identifier,
427 etag,
428 () => {
429 const newContent = asset.content.replace(
430 hashRegExp,
431 (hash) => /** @type {string} */ (hashToNewHash.get(hash))
432 );
433 return new RawSource(newContent);
434 }
435 );
436 }
437 })());
438 };
439 /**
440 * Compute new content without own.
441 * @param {AssetInfoForRealContentHash} asset asset info
442 * @returns {Promise<void>}
443 */
444 const computeNewContentWithoutOwn = (asset) => {
445 if (asset.contentComputeWithoutOwnPromise) {
446 return asset.contentComputeWithoutOwnPromise;
447 }
448 return (asset.contentComputeWithoutOwnPromise = (async () => {
449 if (
450 /** @type {Hashes} */ (asset.ownHashes).size > 0 ||
451 [.../** @type {Hashes} */ (asset.referencedHashes)].some(
452 (hash) => hashToNewHash.get(hash) !== hash
453 )
454 ) {
455 const identifier = `${asset.name}|without-own`;
456 const etag = getEtag(asset);
457 asset.newSourceWithoutOwn = await cacheGenerate.providePromise(
458 identifier,
459 etag,
460 () => {
461 const newContent = asset.content.replace(
462 hashRegExp,
463 (hash) => {
464 if (
465 /** @type {Hashes} */
466 (asset.ownHashes).has(hash)
467 ) {
468 return "";
469 }
470 return /** @type {string} */ (hashToNewHash.get(hash));
471 }
472 );
473 return new RawSource(newContent);
474 }
475 );
476 }
477 })());
478 };
479 /** @type {Comparator<AssetInfoForRealContentHash>} */
480 const comparator = compareSelect((a) => a.name, compareStrings);
481 for (const oldHash of hashesInOrder) {
482 const assets =
483 /** @type {AssetInfoForRealContentHash[]} */
484 (hashToAssets.get(oldHash));
485 assets.sort(comparator);
486 await Promise.all(
487 assets.map((asset) =>
488 /** @type {Hashes} */ (asset.ownHashes).has(oldHash)
489 ? computeNewContentWithoutOwn(asset)
490 : computeNewContent(asset)
491 )
492 );
493 const uniqueChunkArrays = mapAndDeduplicateSourceBuffers(
494 assets,
495 (asset) => {
496 if (/** @type {Hashes} */ (asset.ownHashes).has(oldHash)) {
497 return asset.newSourceWithoutOwn || asset.source;
498 }
499 return asset.newSource || asset.source;
500 }
501 );
502 /** @type {string | undefined} */
503 let newHash;
504 // Only materialize the public `Buffer[]` (one entry per unique
505 // asset) when something is tapped; otherwise the hot path feeds
506 // chunks into the hash directly, avoiding per-asset Buffer.concat.
507 if (hooks.updateHash.isUsed()) {
508 const assetsContent = uniqueChunkArrays.map((chunks) =>
509 chunks.length === 1 ? chunks[0] : Buffer.concat(chunks)
510 );
511 newHash =
512 hooks.updateHash.call(assetsContent, oldHash) || undefined;
513 }
514 if (!newHash) {
515 const hash = createHash(this._hashFunction);
516 if (compilation.outputOptions.hashSalt) {
517 hash.update(compilation.outputOptions.hashSalt);
518 }
519 for (const chunks of uniqueChunkArrays) {
520 for (const c of chunks) hash.update(c);
521 }
522 const digest = hash.digest(this._hashDigest);
523 newHash = digest.slice(0, oldHash.length);
524 }
525 hashToNewHash.set(oldHash, newHash);
526 }
527 await Promise.all(
528 assetsWithInfo.map(async (asset) => {
529 await computeNewContent(asset);
530 const newName = asset.name.replace(
531 hashRegExp,
532 (hash) => /** @type {string} */ (hashToNewHash.get(hash))
533 );
534
535 const infoUpdate = {};
536 const hash =
537 /** @type {Exclude<AssetInfo["contenthash"], undefined>} */
538 (asset.info.contenthash);
539 infoUpdate.contenthash = Array.isArray(hash)
540 ? hash.map(
541 (hash) => /** @type {string} */ (hashToNewHash.get(hash))
542 )
543 : /** @type {string} */ (hashToNewHash.get(hash));
544
545 if (asset.newSource !== undefined) {
546 compilation.updateAsset(
547 asset.name,
548 asset.newSource,
549 infoUpdate
550 );
551 } else {
552 compilation.updateAsset(asset.name, asset.source, infoUpdate);
553 }
554
555 if (asset.name !== newName) {
556 compilation.renameAsset(asset.name, newName);
557 }
558 })
559 );
560 }
561 );
562 });
563 }
564}
565
566module.exports = RealContentHashPlugin;
Note: See TracBrowser for help on using the repository browser.