Skip to content

Commit

Permalink
Merge pull request #14857 from webpack/fix-14839
Browse files Browse the repository at this point in the history
fix asset module hash
  • Loading branch information
sokra committed Feb 28, 2022
2 parents d2c52cb + 936fa78 commit d77b8dd
Show file tree
Hide file tree
Showing 15 changed files with 380 additions and 73 deletions.
2 changes: 1 addition & 1 deletion declarations/WebpackOptions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ export type AssetGeneratorDataUrl =
| AssetGeneratorDataUrlOptions
| AssetGeneratorDataUrlFunction;
/**
* Function that executes for module and should return an DataUrl string.
* Function that executes for module and should return an DataUrl string. It can have a string as 'ident' property which contributes to the module hash.
*/
export type AssetGeneratorDataUrlFunction = (
source: string | Buffer,
Expand Down
58 changes: 41 additions & 17 deletions lib/Compilation.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const Module = require("./Module");
const ModuleDependencyError = require("./ModuleDependencyError");
const ModuleDependencyWarning = require("./ModuleDependencyWarning");
const ModuleGraph = require("./ModuleGraph");
const ModuleHashingError = require("./ModuleHashingError");
const ModuleNotFoundError = require("./ModuleNotFoundError");
const ModuleProfile = require("./ModuleProfile");
const ModuleRestoreError = require("./ModuleRestoreError");
Expand Down Expand Up @@ -3883,6 +3884,7 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
let statModulesFromCache = 0;
const { chunkGraph, runtimeTemplate, moduleMemCaches2 } = this;
const { hashFunction, hashDigest, hashDigestLength } = this.outputOptions;
const errors = [];
for (const module of this.modules) {
const memCache = moduleMemCaches2 && moduleMemCaches2.get(module);
for (const runtime of chunkGraph.getModuleRuntimes(module)) {
Expand All @@ -3907,13 +3909,20 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
hashFunction,
runtimeTemplate,
hashDigest,
hashDigestLength
hashDigestLength,
errors
);
if (memCache) {
memCache.set(`moduleHash-${getRuntimeKey(runtime)}`, digest);
}
}
}
if (errors.length > 0) {
errors.sort(compareSelect(err => err.module, compareModulesByIdentifier));
for (const error of errors) {
this.errors.push(error);
}
}
this.logger.log(
`${statModulesHashed} modules hashed, ${statModulesFromCache} from cache (${
Math.round(
Expand All @@ -3930,17 +3939,22 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
hashFunction,
runtimeTemplate,
hashDigest,
hashDigestLength
hashDigestLength,
errors
) {
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash, {
chunkGraph,
runtime,
runtimeTemplate
});
const moduleHashDigest = /** @type {string} */ (
moduleHash.digest(hashDigest)
);
let moduleHashDigest;
try {
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash, {
chunkGraph,
runtime,
runtimeTemplate
});
moduleHashDigest = /** @type {string} */ (moduleHash.digest(hashDigest));
} catch (err) {
errors.push(new ModuleHashingError(module, err));
moduleHashDigest = "XXXXXX";
}
chunkGraph.setModuleHashes(
module,
runtime,
Expand Down Expand Up @@ -4091,6 +4105,7 @@ This prevents using hashes of each other and should be avoided.`);
const codeGenerationJobs = [];
/** @type {Map<string, Map<Module, {module: Module, hash: string, runtime: RuntimeSpec, runtimes: RuntimeSpec[]}>>} */
const codeGenerationJobsMap = new Map();
const errors = [];

const processChunk = chunk => {
// Last minute module hash generation for modules that depend on chunk hashes
Expand All @@ -4105,7 +4120,8 @@ This prevents using hashes of each other and should be avoided.`);
hashFunction,
runtimeTemplate,
hashDigest,
hashDigestLength
hashDigestLength,
errors
);
let hashMap = codeGenerationJobsMap.get(hash);
if (hashMap) {
Expand All @@ -4129,9 +4145,9 @@ This prevents using hashes of each other and should be avoided.`);
}
}
this.logger.timeAggregate("hashing: hash runtime modules");
this.logger.time("hashing: hash chunks");
const chunkHash = createHash(hashFunction);
try {
this.logger.time("hashing: hash chunks");
const chunkHash = createHash(hashFunction);
if (outputOptions.hashSalt) {
chunkHash.update(outputOptions.hashSalt);
}
Expand Down Expand Up @@ -4162,6 +4178,12 @@ This prevents using hashes of each other and should be avoided.`);
};
otherChunks.forEach(processChunk);
for (const chunk of runtimeChunks) processChunk(chunk);
if (errors.length > 0) {
errors.sort(compareSelect(err => err.module, compareModulesByIdentifier));
for (const error of errors) {
this.errors.push(error);
}
}

this.logger.timeAggregateEnd("hashing: hash runtime modules");
this.logger.timeAggregateEnd("hashing: hash chunks");
Expand Down Expand Up @@ -4801,6 +4823,9 @@ This prevents using hashes of each other and should be avoided.`);
chunkGraph.connectChunkAndModule(chunk, module);
}

/** @type {WebpackError[]} */
const errors = [];

// Hash modules
for (const module of modules) {
this._createModuleHash(
Expand All @@ -4810,15 +4835,14 @@ This prevents using hashes of each other and should be avoided.`);
hashFunction,
runtimeTemplate,
hashDigest,
hashDigestLength
hashDigestLength,
errors
);
}

const codeGenerationResults = new CodeGenerationResults(
this.outputOptions.hashFunction
);
/** @type {WebpackError[]} */
const errors = [];
/**
* @param {Module} module the module
* @param {Callback} callback callback
Expand Down
1 change: 1 addition & 0 deletions lib/Generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
* @property {NormalModule} module the module
* @property {ChunkGraph} chunkGraph
* @property {RuntimeSpec} runtime
* @property {RuntimeTemplate=} runtimeTemplate
*/

/**
Expand Down
29 changes: 29 additions & 0 deletions lib/ModuleHashingError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

const WebpackError = require("./WebpackError");

/** @typedef {import("./Module")} Module */

class ModuleHashingError extends WebpackError {
/**
* Create a new ModuleHashingError
* @param {Module} module related module
* @param {Error} error Original error
*/
constructor(module, error) {
super();

this.name = "ModuleHashingError";
this.error = error;
this.message = error.message;
this.details = error.stack;
this.module = module;
}
}

module.exports = ModuleHashingError;
1 change: 1 addition & 0 deletions lib/RuntimeTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class RuntimeTemplate {
this.outputOptions = outputOptions || {};
this.requestShortener = requestShortener;
this.globalObject = getGlobalObject(outputOptions.globalObject);
this.contentHashReplacement = "X".repeat(outputOptions.hashDigestLength);
}

isIIFE() {
Expand Down
150 changes: 119 additions & 31 deletions lib/asset/AssetGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const decodeDataUriContent = (encoding, content) => {

const JS_TYPES = new Set(["javascript"]);
const JS_AND_ASSET_TYPES = new Set(["javascript", "asset"]);
const DEFAULT_ENCODING = "base64";

class AssetGenerator extends Generator {
/**
Expand All @@ -131,6 +132,65 @@ class AssetGenerator extends Generator {
this.emit = emit;
}

/**
* @param {NormalModule} module module
* @param {RuntimeTemplate} runtimeTemplate runtime template
* @returns {string} source file name
*/
getSourceFileName(module, runtimeTemplate) {
return makePathsRelative(
runtimeTemplate.compilation.compiler.context,
module.matchResource || module.resource,
runtimeTemplate.compilation.compiler.root
).replace(/^\.\//, "");
}

/**
* @param {NormalModule} module module
* @returns {string} mime type
*/
getMimeType(module) {
if (typeof this.dataUrlOptions === "function") {
throw new Error(
"This method must not be called when dataUrlOptions is a function"
);
}

let mimeType = this.dataUrlOptions.mimetype;
if (mimeType === undefined) {
const ext = path.extname(module.nameForCondition());
if (
module.resourceResolveData &&
module.resourceResolveData.mimetype !== undefined
) {
mimeType =
module.resourceResolveData.mimetype +
module.resourceResolveData.parameters;
} else if (ext) {
mimeType = mimeTypes.lookup(ext);

if (typeof mimeType !== "string") {
throw new Error(
"DataUrl can't be generated automatically, " +
`because there is no mimetype for "${ext}" in mimetype database. ` +
'Either pass a mimetype via "generator.mimetype" or ' +
'use type: "asset/resource" to create a resource file instead of a DataUrl'
);
}
}
}

if (typeof mimeType !== "string") {
throw new Error(
"DataUrl can't be generated automatically. " +
'Either pass a mimetype via "generator.mimetype" or ' +
'use type: "asset/resource" to create a resource file instead of a DataUrl'
);
}

return mimeType;
}

/**
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
Expand Down Expand Up @@ -170,31 +230,9 @@ class AssetGenerator extends Generator {
}
}
if (encoding === undefined) {
encoding = "base64";
}
let ext;
let mimeType = this.dataUrlOptions.mimetype;
if (mimeType === undefined) {
ext = path.extname(module.nameForCondition());
if (
module.resourceResolveData &&
module.resourceResolveData.mimetype !== undefined
) {
mimeType =
module.resourceResolveData.mimetype +
module.resourceResolveData.parameters;
} else if (ext) {
mimeType = mimeTypes.lookup(ext);
}
}
if (typeof mimeType !== "string") {
throw new Error(
"DataUrl can't be generated automatically, " +
`because there is no mimetype for "${ext}" in mimetype database. ` +
'Either pass a mimetype via "generator.mimetype" or ' +
'use type: "asset/resource" to create a resource file instead of a DataUrl'
);
encoding = DEFAULT_ENCODING;
}
const mimeType = this.getMimeType(module);

let encodedContent;

Expand Down Expand Up @@ -238,11 +276,10 @@ class AssetGenerator extends Generator {
runtimeTemplate.outputOptions.hashDigestLength
);
module.buildInfo.fullContentHash = fullHash;
const sourceFilename = makePathsRelative(
runtimeTemplate.compilation.compiler.context,
module.matchResource || module.resource,
runtimeTemplate.compilation.compiler.root
).replace(/^\.\//, "");
const sourceFilename = this.getSourceFileName(
module,
runtimeTemplate
);
let { path: filename, info: assetInfo } =
runtimeTemplate.compilation.getAssetPathWithInfo(
assetModuleFilename,
Expand Down Expand Up @@ -368,8 +405,59 @@ class AssetGenerator extends Generator {
* @param {Hash} hash hash that will be modified
* @param {UpdateHashContext} updateHashContext context for updating hash
*/
updateHash(hash, { module }) {
hash.update(module.buildInfo.dataUrl ? "data-url" : "resource");
updateHash(hash, { module, runtime, runtimeTemplate, chunkGraph }) {
if (module.buildInfo.dataUrl) {
hash.update("data-url");
// this.dataUrlOptions as function should be pure and only depend on input source and filename
// therefore it doesn't need to be hashed
if (typeof this.dataUrlOptions === "function") {
const ident = /** @type {{ ident?: string }} */ (this.dataUrlOptions)
.ident;
if (ident) hash.update(ident);
} else {
if (
this.dataUrlOptions.encoding &&
this.dataUrlOptions.encoding !== DEFAULT_ENCODING
) {
hash.update(this.dataUrlOptions.encoding);
}
if (this.dataUrlOptions.mimetype)
hash.update(this.dataUrlOptions.mimetype);
// computed mimetype depends only on module filename which is already part of the hash
}
} else {
hash.update("resource");

const pathData = {
module,
runtime,
filename: this.getSourceFileName(module, runtimeTemplate),
chunkGraph,
contentHash: runtimeTemplate.contentHashReplacement
};

if (typeof this.publicPath === "function") {
hash.update("path");
const assetInfo = {};
hash.update(this.publicPath(pathData, assetInfo));
hash.update(JSON.stringify(assetInfo));
} else if (this.publicPath) {
hash.update("path");
hash.update(this.publicPath);
} else {
hash.update("no-path");
}

const assetModuleFilename =
this.filename || runtimeTemplate.outputOptions.assetModuleFilename;
const { path: filename, info } =
runtimeTemplate.compilation.getAssetPathWithInfo(
assetModuleFilename,
pathData
);
hash.update(filename);
hash.update(JSON.stringify(info));
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion schemas/WebpackOptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
]
},
"AssetGeneratorDataUrlFunction": {
"description": "Function that executes for module and should return an DataUrl string.",
"description": "Function that executes for module and should return an DataUrl string. It can have a string as 'ident' property which contributes to the module hash.",
"instanceof": "Function",
"tsType": "((source: string | Buffer, context: { filename: string, module: import('../lib/Module') }) => string)"
},
Expand Down

0 comments on commit d77b8dd

Please sign in to comment.