fix: use a valid output path for errored asset modules (#19281)

This commit is contained in:
Alexander Akait 2025-03-05 17:41:19 +03:00 committed by GitHub
parent 05f2862b32
commit 98221f239b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 303 additions and 166 deletions

View File

@ -32,7 +32,9 @@ const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
/** @typedef {import("../../declarations/WebpackOptions").AssetGeneratorOptions} AssetGeneratorOptions */
/** @typedef {import("../../declarations/WebpackOptions").AssetModuleFilename} AssetModuleFilename */
/** @typedef {import("../../declarations/WebpackOptions").AssetModuleOutputPath} AssetModuleOutputPath */
/** @typedef {import("../../declarations/WebpackOptions").AssetResourceGeneratorOptions} AssetResourceGeneratorOptions */
/** @typedef {import("../../declarations/WebpackOptions").RawPublicPath} RawPublicPath */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Compilation").AssetInfo} AssetInfo */
/** @typedef {import("../Compilation").InterpolatedPathAndAssetInfo} InterpolatedPathAndAssetInfo */
@ -50,6 +52,7 @@ const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
/** @typedef {import("../TemplatedPathPlugin").TemplatePath} TemplatePath */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/createHash").Algorithm} Algorithm */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
/**
* @template T
@ -212,7 +215,7 @@ class AssetGenerator extends Generator {
* @param {RuntimeTemplate} runtimeTemplate runtime template
* @returns {string} source file name
*/
getSourceFileName(module, runtimeTemplate) {
static getSourceFileName(module, runtimeTemplate) {
return makePathsRelative(
runtimeTemplate.compilation.compiler.context,
module.matchResource || module.resource,
@ -220,6 +223,176 @@ class AssetGenerator extends Generator {
).replace(/^\.\//, "");
}
/**
* @param {NormalModule} module module
* @param {RuntimeTemplate} runtimeTemplate runtime template
* @returns {[string, string]} return full hash and non-numeric full hash
*/
static getFullContentHash(module, runtimeTemplate) {
const hash = createHash(
/** @type {Algorithm} */
(runtimeTemplate.outputOptions.hashFunction)
);
if (runtimeTemplate.outputOptions.hashSalt) {
hash.update(runtimeTemplate.outputOptions.hashSalt);
}
const source = module.originalSource();
if (source) {
hash.update(source.buffer());
}
const fullContentHash = /** @type {string} */ (
hash.digest(runtimeTemplate.outputOptions.hashDigest)
);
/** @type {string} */
const contentHash = nonNumericOnlyHash(
fullContentHash,
/** @type {number} */
(runtimeTemplate.outputOptions.hashDigestLength)
);
return [fullContentHash, contentHash];
}
/**
* @param {NormalModule} module module for which the code should be generated
* @param {Pick<AssetResourceGeneratorOptions, "filename" | "outputPath">} generatorOptions generator options
* @param {{ runtime: RuntimeSpec, runtimeTemplate: RuntimeTemplate, chunkGraph: ChunkGraph }} generateContext context for generate
* @param {string} contentHash the content hash
* @returns {{ filename: string, originalFilename: string, assetInfo: AssetInfo }} info
*/
static getFilenameWithInfo(
module,
generatorOptions,
{ runtime, runtimeTemplate, chunkGraph },
contentHash
) {
const assetModuleFilename =
generatorOptions.filename ||
/** @type {AssetModuleFilename} */
(runtimeTemplate.outputOptions.assetModuleFilename);
const sourceFilename = AssetGenerator.getSourceFileName(
module,
runtimeTemplate
);
let { path: filename, info: assetInfo } =
runtimeTemplate.compilation.getAssetPathWithInfo(assetModuleFilename, {
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
});
const originalFilename = filename;
if (generatorOptions.outputPath) {
const { path: outputPath, info } =
runtimeTemplate.compilation.getAssetPathWithInfo(
generatorOptions.outputPath,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
filename = path.posix.join(outputPath, filename);
assetInfo = mergeAssetInfo(assetInfo, info);
}
return { originalFilename, filename, assetInfo };
}
/**
* @param {NormalModule} module module for which the code should be generated
* @param {Pick<AssetResourceGeneratorOptions, "publicPath">} generatorOptions generator options
* @param {GenerateContext} generateContext context for generate
* @param {string} filename the filename
* @param {AssetInfo} assetInfo the asset info
* @param {string} contentHash the content hash
* @returns {{ assetPath: string, assetInfo: AssetInfo }} asset path and info
*/
static getAssetPathWithInfo(
module,
generatorOptions,
{ runtime, runtimeTemplate, type, chunkGraph, runtimeRequirements },
filename,
assetInfo,
contentHash
) {
const sourceFilename = AssetGenerator.getSourceFileName(
module,
runtimeTemplate
);
let assetPath;
if (generatorOptions.publicPath !== undefined && type === "javascript") {
const { path, info } = runtimeTemplate.compilation.getAssetPathWithInfo(
generatorOptions.publicPath,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
assetInfo = mergeAssetInfo(assetInfo, info);
assetPath = JSON.stringify(path + filename);
} else if (
generatorOptions.publicPath !== undefined &&
type === "css-url"
) {
const { path, info } = runtimeTemplate.compilation.getAssetPathWithInfo(
generatorOptions.publicPath,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
assetInfo = mergeAssetInfo(assetInfo, info);
assetPath = path + filename;
} else if (type === "javascript") {
// add __webpack_require__.p
runtimeRequirements.add(RuntimeGlobals.publicPath);
assetPath = runtimeTemplate.concatenation(
{ expr: RuntimeGlobals.publicPath },
filename
);
} else if (type === "css-url") {
const compilation = runtimeTemplate.compilation;
const path =
compilation.outputOptions.publicPath === "auto"
? CssUrlDependency.PUBLIC_PATH_AUTO
: compilation.getAssetPath(
/** @type {TemplatePath} */
(compilation.outputOptions.publicPath),
{
hash: compilation.hash
}
);
assetPath = path + filename;
}
return {
// eslint-disable-next-line object-shorthand
assetPath: /** @type {string} */ (assetPath),
assetInfo: { sourceFilename, ...assetInfo }
};
}
/**
* @param {NormalModule} module module for which the bailout reason should be determined
* @param {ConcatenationBailoutReasonContext} context context
@ -335,127 +508,6 @@ class AssetGenerator extends Generator {
return encodedSource;
}
/**
* @private
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
* @param {string} contentHash the content hash
* @returns {{ filename: string, originalFilename: string, assetInfo: AssetInfo }} info
*/
_getFilenameWithInfo(
module,
{ runtime, runtimeTemplate, chunkGraph },
contentHash
) {
const assetModuleFilename =
this.filename ||
/** @type {AssetModuleFilename} */
(runtimeTemplate.outputOptions.assetModuleFilename);
const sourceFilename = this.getSourceFileName(module, runtimeTemplate);
let { path: filename, info: assetInfo } =
runtimeTemplate.compilation.getAssetPathWithInfo(assetModuleFilename, {
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
});
const originalFilename = filename;
if (this.outputPath) {
const { path: outputPath, info } =
runtimeTemplate.compilation.getAssetPathWithInfo(this.outputPath, {
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
});
filename = path.posix.join(outputPath, filename);
assetInfo = mergeAssetInfo(assetInfo, info);
}
return { originalFilename, filename, assetInfo };
}
/**
* @private
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
* @param {string} filename the filename
* @param {AssetInfo} assetInfo the asset info
* @param {string} contentHash the content hash
* @returns {{ assetPath: string, assetInfo: AssetInfo }} asset path and info
*/
_getAssetPathWithInfo(
module,
{ runtimeTemplate, runtime, chunkGraph, type, runtimeRequirements },
filename,
assetInfo,
contentHash
) {
const sourceFilename = this.getSourceFileName(module, runtimeTemplate);
let assetPath;
if (this.publicPath !== undefined && type === "javascript") {
const { path, info } = runtimeTemplate.compilation.getAssetPathWithInfo(
this.publicPath,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
assetInfo = mergeAssetInfo(assetInfo, info);
assetPath = JSON.stringify(path + filename);
} else if (this.publicPath !== undefined && type === "css-url") {
const { path, info } = runtimeTemplate.compilation.getAssetPathWithInfo(
this.publicPath,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
assetInfo = mergeAssetInfo(assetInfo, info);
assetPath = path + filename;
} else if (type === "javascript") {
// add __webpack_require__.p
runtimeRequirements.add(RuntimeGlobals.publicPath);
assetPath = runtimeTemplate.concatenation(
{ expr: RuntimeGlobals.publicPath },
filename
);
} else if (type === "css-url") {
const compilation = runtimeTemplate.compilation;
const path =
compilation.outputOptions.publicPath === "auto"
? CssUrlDependency.PUBLIC_PATH_AUTO
: compilation.getAssetPath(
/** @type {TemplatePath} */
(compilation.outputOptions.publicPath),
{
hash: compilation.hash
}
);
assetPath = path + filename;
}
return {
// eslint-disable-next-line object-shorthand
assetPath: /** @type {string} */ (assetPath),
assetInfo: { sourceFilename, ...assetInfo }
};
}
/**
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
@ -489,53 +541,40 @@ class AssetGenerator extends Generator {
data.set("url", { [type]: content, ...data.get("url") });
}
} else {
const hash = createHash(
/** @type {Algorithm} */
(runtimeTemplate.outputOptions.hashFunction)
);
if (runtimeTemplate.outputOptions.hashSalt) {
hash.update(runtimeTemplate.outputOptions.hashSalt);
}
hash.update(/** @type {Source} */ (module.originalSource()).buffer());
const fullHash =
/** @type {string} */
(hash.digest(runtimeTemplate.outputOptions.hashDigest));
if (data) {
data.set("fullContentHash", fullHash);
}
/** @type {BuildInfo} */
(module.buildInfo).fullContentHash = fullHash;
/** @type {string} */
const contentHash = nonNumericOnlyHash(
fullHash,
/** @type {number} */
(generateContext.runtimeTemplate.outputOptions.hashDigestLength)
const [fullContentHash, contentHash] = AssetGenerator.getFullContentHash(
module,
runtimeTemplate
);
if (data) {
data.set("fullContentHash", fullContentHash);
data.set("contentHash", contentHash);
}
/** @type {BuildInfo} */
(module.buildInfo).fullContentHash = fullContentHash;
const { originalFilename, filename, assetInfo } =
this._getFilenameWithInfo(module, generateContext, contentHash);
AssetGenerator.getFilenameWithInfo(
module,
{ filename: this.filename, outputPath: this.outputPath },
generateContext,
contentHash
);
if (data) {
data.set("filename", filename);
}
let { assetPath, assetInfo: newAssetInfo } = this._getAssetPathWithInfo(
module,
generateContext,
originalFilename,
assetInfo,
contentHash
);
let { assetPath, assetInfo: newAssetInfo } =
AssetGenerator.getAssetPathWithInfo(
module,
{ publicPath: this.publicPath },
generateContext,
originalFilename,
assetInfo,
contentHash
);
if (data && (type === "javascript" || type === "css-url")) {
data.set("url", { [type]: assetPath, ...data.get("url") });
@ -704,7 +743,7 @@ class AssetGenerator extends Generator {
const pathData = {
module,
runtime,
filename: this.getSourceFileName(module, runtimeTemplate),
filename: AssetGenerator.getSourceFileName(module, runtimeTemplate),
chunkGraph,
contentHash: runtimeTemplate.contentHashReplacement
};

View File

@ -19,10 +19,12 @@ const memoize = require("../util/memoize");
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../../declarations/WebpackOptions").AssetParserOptions} AssetParserOptions */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../Compilation").AssetInfo} AssetInfo */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../Module").BuildInfo} BuildInfo */
/** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
/** @typedef {import("../NormalModule")} NormalModule */
/**
* @param {string} name name of definitions
@ -184,7 +186,7 @@ class AssetModulesPlugin {
compilation.hooks.renderManifest.tap(plugin, (result, options) => {
const { chunkGraph } = compilation;
const { chunk, codeGenerationResults } = options;
const { chunk, codeGenerationResults, runtimeTemplate } = options;
const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType(
chunk,
@ -203,18 +205,58 @@ class AssetModulesPlugin {
/** @type {NonNullable<CodeGenerationResult["data"]>} */
(codeGenResult.data);
const errored = module.getNumberOfErrors() > 0;
/** @type {string} */
let entryFilename;
/** @type {AssetInfo} */
let entryInfo;
/** @type {string} */
let entryHash;
if (errored) {
const erroredModule = /** @type {NormalModule} */ (module);
const AssetGenerator = getAssetGenerator();
const [fullContentHash, contentHash] =
AssetGenerator.getFullContentHash(
erroredModule,
runtimeTemplate
);
const { filename, assetInfo } =
AssetGenerator.getFilenameWithInfo(
erroredModule,
{
filename:
erroredModule.generatorOptions &&
erroredModule.generatorOptions.filename,
outputPath:
erroredModule.generatorOptions &&
erroredModule.generatorOptions.outputPath
},
{
runtime: chunk.runtime,
runtimeTemplate,
chunkGraph
},
contentHash
);
entryFilename = filename;
entryInfo = assetInfo;
entryHash = fullContentHash;
} else {
entryFilename = buildInfo.filename || data.get("filename");
entryInfo = buildInfo.assetInfo || data.get("assetInfo");
entryHash =
buildInfo.fullContentHash || data.get("fullContentHash");
}
result.push({
render: () =>
/** @type {Source} */ (codeGenResult.sources.get(type)),
filename: errored
? module.nameForCondition()
: buildInfo.filename || data.get("filename"),
info: buildInfo.assetInfo || data.get("assetInfo"),
filename: entryFilename,
info: entryInfo,
auxiliary: true,
identifier: `assetModule${chunkGraph.getModuleId(module)}`,
hash: errored
? chunkGraph.getModuleHash(module, chunk.runtime)
: buildInfo.fullContentHash || data.get("fullContentHash")
hash: entryHash
});
} catch (err) {
/** @type {Error} */ (err).message +=

View File

@ -0,0 +1 @@
module.exports = [/Error from loader/];

View File

@ -0,0 +1,7 @@
it("should use a valid output path", () => {
try {
new URL("./style.css", import.meta.url);
} catch (e) {
// Nothing
}
});

View File

@ -0,0 +1,7 @@
module.exports = options => {
if (options.cache && options.cache.type === "filesystem") {
return [/Pack got invalid because of write to/];
}
return [];
};

View File

@ -0,0 +1,3 @@
module.exports = function loader() {
throw new Error("Error from loader");
};

View File

@ -0,0 +1,3 @@
a {
color: red;
}

View File

@ -0,0 +1,12 @@
const fs = require("fs");
const path = require("path");
module.exports = {
afterExecute(options) {
const files = fs.readdirSync(path.resolve(options.output.path, "./css"));
if (!/style\.[0-9a-f]{8}\.css/.test(files[0])) {
throw new Error(`Invalid path for ${files.join(",")} files.`);
}
}
};

View File

@ -0,0 +1,23 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
output: {
hashDigestLength: 8
},
module: {
rules: [
{
test: /\.css$/i,
type: "asset/resource",
generator: {
filename: () => `css/style.[contenthash].css`
},
use: [
{
loader: require.resolve("./loader")
}
]
}
]
}
};