webpack/lib/optimize/RealContentHashPlugin.js

259 lines
7.2 KiB
JavaScript
Raw Normal View History

/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const { RawSource } = require("webpack-sources");
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();
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, "\\$&");
};
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(
{
name: "RealContentHashPlugin",
stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH
},
2020-08-19 17:25:53 +08:00
async () => {
const assets = compilation.getAssets();
const assetsWithInfo = [];
const hashToAssets = new Map();
for (const { source, info, name } of assets) {
const content = source.source();
/** @type {Set<string>} */
const hashes = new Set();
addToList(info.contenthash, hashes);
const data = {
name,
info,
source,
2020-08-19 17:25:53 +08:00
/** @type {RawSource | undefined} */
2020-08-19 15:46:41 +08:00
newSource: undefined,
content,
hasOwnHash: false,
2020-08-19 17:25:53 +08:00
contentComputePromise: false,
/** @type {Set<string>} */
referencedHashes: undefined,
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;
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 17:25:53 +08:00
const etag = cacheAnalyse.mergeEtags(
cacheAnalyse.getLazyHashedEtag(source),
Array.from(hashes).join("|")
);
asset.referencedHashes = await cacheAnalyse.providePromise(
name,
etag,
() => {
const referencedHashes = new Set();
const inContent = content.match(hashRegExp);
if (inContent) {
for (const hash of inContent) {
if (hashes.has(hash)) {
asset.hasOwnHash = true;
continue;
}
referencedHashes.add(hash);
}
}
return referencedHashes;
}
);
})
);
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);
}
);
}
})());
};
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));
for (const asset of assets) {
2020-08-19 17:25:53 +08:00
hash.update(
asset.newSource
? asset.newSource.buffer()
: asset.source.buffer()
);
}
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 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 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 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 17:25:53 +08:00
if (asset.name !== newName) {
compilation.renameAsset(asset.name, newName);
}
})
);
}
);
});
}
}
module.exports = RealContentHashPlugin;