fix(css): avoid extra runtime for assets modules

This commit is contained in:
alexander.akait 2024-10-11 12:59:00 +03:00
parent fd75e12ff2
commit 485f3d6758
7 changed files with 436 additions and 233 deletions

View File

@ -816,7 +816,9 @@ class ExternalModule extends Module {
new RawSource(`module.exports = ${JSON.stringify(request)};`)
);
const data = new Map();
data.set("url", request);
data.set("url", {
javascript: request
});
return { sources, runtimeRequirements: RUNTIME_REQUIREMENTS, data };
}
case "css-import": {

View File

@ -1097,27 +1097,6 @@ class RuntimeTemplate {
runtimeRequirements.add(RuntimeGlobals.exports);
return `${RuntimeGlobals.makeNamespaceObject}(${exportsArgument});\n`;
}
/**
* @param {object} options options object
* @param {Module} options.module the module
* @param {RuntimeSpec=} options.runtime runtime
* @param {CodeGenerationResults} options.codeGenerationResults the code generation results
* @returns {string} the url of the asset
*/
assetUrl({ runtime, module, codeGenerationResults }) {
if (!module) {
return "data:,";
}
const codeGen = codeGenerationResults.get(module, runtime);
const data = /** @type {NonNullable<CodeGenerationResult["data"]>} */ (
codeGen.data
);
const url = data.get("url");
if (url) return url.toString();
const assetPath = data.get("assetPathForCss");
return assetPath;
}
}
module.exports = RuntimeTemplate;

View File

@ -24,12 +24,16 @@ const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
/** @typedef {import("../../declarations/WebpackOptions").AssetModuleOutputPath} AssetModuleOutputPath */
/** @typedef {import("../../declarations/WebpackOptions").RawPublicPath} RawPublicPath */
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Compilation").AssetInfo} AssetInfo */
/** @typedef {import("../Compilation").InterpolatedPathAndAssetInfo} InterpolatedPathAndAssetInfo */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Generator").GenerateContext} GenerateContext */
/** @typedef {import("../Generator").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../Module").BuildInfo} BuildInfo */
/** @typedef {import("../Module").BuildMeta} BuildMeta */
/** @typedef {import("../Module").ConcatenationBailoutReasonContext} ConcatenationBailoutReasonContext */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../NormalModule")} NormalModule */
/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
/** @typedef {import("../TemplatedPathPlugin").TemplatePath} TemplatePath */
@ -164,25 +168,44 @@ const decodeDataUriContent = (encoding, content) => {
}
};
const NO_TYPES = new Set();
const ASSET_TYPES = new Set([ASSET_MODULE_TYPE]);
const JS_TYPES = new Set(["javascript"]);
const JS_AND_ASSET_TYPES = new Set(["javascript", ASSET_MODULE_TYPE]);
const CSS_TYPES = new Set(["css-url"]);
const CSS_AND_JS_TYPES = new Set(["javascript", "css-url"]);
const JS_AND_ASSET_TYPES = new Set([ASSET_MODULE_TYPE, "javascript"]);
const CSS_AND_ASSET_TYPES = new Set([ASSET_MODULE_TYPE, "css-url"]);
const JS_AND_CSS_AND_ASSET_TYPES = new Set([
ASSET_MODULE_TYPE,
"javascript",
"css-url"
]);
const DEFAULT_ENCODING = "base64";
class AssetGenerator extends Generator {
/**
* @param {ModuleGraph} moduleGraph the module graph
* @param {AssetGeneratorOptions["dataUrl"]=} dataUrlOptions the options for the data url
* @param {AssetModuleFilename=} filename override for output.assetModuleFilename
* @param {RawPublicPath=} publicPath override for output.assetModulePublicPath
* @param {AssetModuleOutputPath=} outputPath the output path for the emitted file which is not included in the runtime import
* @param {boolean=} emit generate output asset
*/
constructor(dataUrlOptions, filename, publicPath, outputPath, emit) {
constructor(
moduleGraph,
dataUrlOptions,
filename,
publicPath,
outputPath,
emit
) {
super();
this.dataUrlOptions = dataUrlOptions;
this.filename = filename;
this.publicPath = publicPath;
this.outputPath = outputPath;
this.emit = emit;
this._moduleGraph = moduleGraph;
}
/**
@ -259,208 +282,368 @@ class AssetGenerator extends Generator {
return /** @type {string} */ (mimeType);
}
/**
* @param {NormalModule} module module for which the code should be generated
* @returns {string} DataURI
*/
generateDataUri(module) {
const source = /** @type {Source} */ (module.originalSource());
let encodedSource;
if (typeof this.dataUrlOptions === "function") {
encodedSource = this.dataUrlOptions.call(null, source.source(), {
filename: module.matchResource || module.resource,
module
});
} else {
/** @type {"base64" | false | undefined} */
let encoding =
/** @type {AssetGeneratorDataUrlOptions} */
(this.dataUrlOptions).encoding;
if (
encoding === undefined &&
module.resourceResolveData &&
module.resourceResolveData.encoding !== undefined
) {
encoding = module.resourceResolveData.encoding;
}
if (encoding === undefined) {
encoding = DEFAULT_ENCODING;
}
const mimeType = this.getMimeType(module);
let encodedContent;
if (
module.resourceResolveData &&
module.resourceResolveData.encoding === encoding &&
decodeDataUriContent(
module.resourceResolveData.encoding,
module.resourceResolveData.encodedContent
).equals(source.buffer())
) {
encodedContent = module.resourceResolveData.encodedContent;
} else {
encodedContent = encodeDataUri(encoding, source);
}
encodedSource = `data:${mimeType}${
encoding ? `;${encoding}` : ""
},${encodedContent}`;
}
return encodedSource;
}
/**
* @private
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
* @returns {string} the full content hash
*/
_getFullContentHash(module, { runtimeTemplate }) {
const hash = createHash(
/** @type {Algorithm} */
(runtimeTemplate.outputOptions.hashFunction)
);
if (runtimeTemplate.outputOptions.hashSalt) {
hash.update(runtimeTemplate.outputOptions.hashSalt);
}
hash.update(/** @type {Source} */ (module.originalSource()).buffer());
return /** @type {string} */ (
hash.digest(runtimeTemplate.outputOptions.hashDigest)
);
}
/**
* @private
* @param {string} fullContentHash the full content hash
* @param {GenerateContext} generateContext context for generate
* @returns {string} the content hash
*/
_getContentHash(fullContentHash, generateContext) {
return nonNumericOnlyHash(
fullContentHash,
/** @type {number} */
(generateContext.runtimeTemplate.outputOptions.hashDigestLength)
);
}
/**
* @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;
}
assetInfo = { sourceFilename, ...assetInfo };
return { assetPath, assetInfo };
}
/**
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
* @returns {Source} generated code
*/
generate(
module,
{
runtime,
concatenationScope,
chunkGraph,
generate(module, generateContext) {
const {
type,
getData,
runtimeTemplate,
runtimeRequirements,
type,
getData
}
) {
switch (type) {
case ASSET_MODULE_TYPE:
return /** @type {Source} */ (module.originalSource());
default: {
let content;
const originalSource = /** @type {Source} */ (module.originalSource());
if (
/** @type {BuildInfo} */
(module.buildInfo).dataUrl
) {
let encodedSource;
if (typeof this.dataUrlOptions === "function") {
encodedSource = this.dataUrlOptions.call(
null,
originalSource.source(),
{
filename: module.matchResource || module.resource,
module
}
);
} else {
/** @type {"base64" | false | undefined} */
let encoding =
/** @type {AssetGeneratorDataUrlOptions} */
(this.dataUrlOptions).encoding;
if (
encoding === undefined &&
module.resourceResolveData &&
module.resourceResolveData.encoding !== undefined
) {
encoding = module.resourceResolveData.encoding;
}
if (encoding === undefined) {
encoding = DEFAULT_ENCODING;
}
const mimeType = this.getMimeType(module);
concatenationScope
} = generateContext;
let encodedContent;
let content;
if (
module.resourceResolveData &&
module.resourceResolveData.encoding === encoding &&
decodeDataUriContent(
module.resourceResolveData.encoding,
module.resourceResolveData.encodedContent
).equals(originalSource.buffer())
) {
encodedContent = module.resourceResolveData.encodedContent;
} else {
encodedContent = encodeDataUri(encoding, originalSource);
}
const needContent = type === "javascript" || type === "css-url";
encodedSource = `data:${mimeType}${
encoding ? `;${encoding}` : ""
},${encodedContent}`;
}
const data =
/** @type {NonNullable<GenerateContext["getData"]>} */
(getData)();
data.set("url", Buffer.from(encodedSource));
content = JSON.stringify(encodedSource);
} else {
const assetModuleFilename =
this.filename ||
/** @type {AssetModuleFilename} */
(runtimeTemplate.outputOptions.assetModuleFilename);
const hash = createHash(
/** @type {Algorithm} */
(runtimeTemplate.outputOptions.hashFunction)
);
if (runtimeTemplate.outputOptions.hashSalt) {
hash.update(runtimeTemplate.outputOptions.hashSalt);
}
hash.update(originalSource.buffer());
const fullHash = /** @type {string} */ (
hash.digest(runtimeTemplate.outputOptions.hashDigest)
);
const contentHash = nonNumericOnlyHash(
fullHash,
/** @type {number} */
(runtimeTemplate.outputOptions.hashDigestLength)
);
/** @type {BuildInfo} */
(module.buildInfo).fullContentHash = fullHash;
const sourceFilename = this.getSourceFileName(
module,
runtimeTemplate
);
let { path: filename, info: assetInfo } =
runtimeTemplate.compilation.getAssetPathWithInfo(
assetModuleFilename,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
let assetPath;
let assetPathForCss;
if (this.publicPath !== undefined) {
const { path, info } =
runtimeTemplate.compilation.getAssetPathWithInfo(
this.publicPath,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
assetInfo = mergeAssetInfo(assetInfo, info);
assetPath = JSON.stringify(path + filename);
assetPathForCss = path + filename;
} else {
runtimeRequirements.add(RuntimeGlobals.publicPath); // add __webpack_require__.p
assetPath = runtimeTemplate.concatenation(
{ expr: RuntimeGlobals.publicPath },
filename
);
const compilation = runtimeTemplate.compilation;
const path =
compilation.outputOptions.publicPath === "auto"
? CssUrlDependency.PUBLIC_PATH_AUTO
: compilation.getAssetPath(
/** @type {TemplatePath} */
(compilation.outputOptions.publicPath),
{
hash: compilation.hash
}
);
assetPathForCss = path + filename;
}
assetInfo = {
sourceFilename,
...assetInfo
};
if (this.outputPath) {
const { path: outputPath, info } =
runtimeTemplate.compilation.getAssetPathWithInfo(
this.outputPath,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
assetInfo = mergeAssetInfo(assetInfo, info);
filename = path.posix.join(outputPath, filename);
}
/** @type {BuildInfo} */
(module.buildInfo).filename = filename;
/** @type {BuildInfo} */
(module.buildInfo).assetInfo = assetInfo;
if (getData) {
// Due to code generation caching module.buildInfo.XXX can't used to store such information
// It need to be stored in the code generation results instead, where it's cached too
// TODO webpack 6 For back-compat reasons we also store in on module.buildInfo
const data = getData();
data.set("fullContentHash", fullHash);
data.set("filename", filename);
data.set("assetInfo", assetInfo);
data.set("assetPathForCss", assetPathForCss);
}
content = assetPath;
const data = getData
? /** @type {GenerateContext["getData"]} */
(getData)()
: undefined;
if (
/** @type {BuildInfo} */
(module.buildInfo).dataUrl &&
needContent
) {
if (data && data.has("url")) {
content = data.get("url");
} else {
const encodedSource = this.generateDataUri(module);
content = JSON.stringify(encodedSource);
if (data) {
data.set("url", {
[type]: Buffer.from(encodedSource)
});
}
if (concatenationScope) {
concatenationScope.registerNamespaceExport(
ConcatenationScope.NAMESPACE_OBJECT_EXPORT
);
return new RawSource(
`${runtimeTemplate.supportsConst() ? "const" : "var"} ${
ConcatenationScope.NAMESPACE_OBJECT_EXPORT
} = ${content};`
);
}
runtimeRequirements.add(RuntimeGlobals.module);
return new RawSource(`${RuntimeGlobals.module}.exports = ${content};`);
}
} else {
/** @type {string} */
let fullHash;
if (data && data.has("fullContentHash")) {
fullHash = data.get("fullContentHash");
} else {
fullHash = this._getFullContentHash(module, generateContext);
if (data) {
data.set("fullContentHash", fullHash);
}
}
if (!(/** @type {BuildInfo} */ (module.buildInfo).fullContentHash)) {
/** @type {BuildInfo} */
(module.buildInfo).fullContentHash = fullHash;
}
/** @type {string} */
let contentHash;
if (data && data.has("contentHash")) {
contentHash = data.get("contentHash");
} else {
contentHash = this._getContentHash(fullHash, generateContext);
if (data) {
data.set("contentHash", contentHash);
}
}
let originalFilename;
let filename;
let assetInfo = {};
if (data && data.has("originalFilename")) {
originalFilename = data.get("originalFilename");
} else {
({ originalFilename, filename, assetInfo } = this._getFilenameWithInfo(
module,
generateContext,
contentHash
));
if (data) {
data.set("filename", filename);
data.set("assetInfo", assetInfo);
data.set("filenameWithInfo", originalFilename);
}
}
assetInfo =
data && data.has("assetInfo") ? data.get("assetInfo") : assetInfo;
/** @type {string} */
let assetPath;
if (type === "javascript" || type === "css-url") {
({ assetPath, assetInfo } = this._getAssetPathWithInfo(
module,
generateContext,
originalFilename,
assetInfo,
contentHash
));
if (data) {
data.set("url", { [type]: assetPath, ...data.get("url") });
}
}
if (data) {
data.set("assetInfo", assetInfo);
}
// Due to code generation caching module.buildInfo.XXX can't used to store such information
// It need to be stored in the code generation results instead, where it's cached too
// TODO webpack 6 For back-compat reasons we also store in on module.buildInfo
if (!module.buildInfo.filename) {
/** @type {BuildInfo} */
(module.buildInfo).filename = filename;
}
if (!module.buildInfo.assetInfo) {
/** @type {BuildInfo} */
(module.buildInfo).assetInfo = assetInfo;
}
content = assetPath;
}
if (type === "javascript") {
if (concatenationScope) {
concatenationScope.registerNamespaceExport(
ConcatenationScope.NAMESPACE_OBJECT_EXPORT
);
return new RawSource(
`${runtimeTemplate.supportsConst() ? "const" : "var"} ${
ConcatenationScope.NAMESPACE_OBJECT_EXPORT
} = ${content};`
);
}
runtimeRequirements.add(RuntimeGlobals.module);
return new RawSource(`${RuntimeGlobals.module}.exports = ${content};`);
} else if (type === "css-url") {
return null;
}
return /** @type {Source} */ (module.originalSource());
}
/**
@ -468,10 +651,38 @@ class AssetGenerator extends Generator {
* @returns {Set<string>} available types (do not mutate)
*/
getTypes(module) {
if ((module.buildInfo && module.buildInfo.dataUrl) || this.emit === false) {
return JS_TYPES;
const sourceTypes = new Set();
const connections = this._moduleGraph.getIncomingConnections(module);
for (const connection of connections) {
sourceTypes.add(connection.originModule.type.split("/")[0]);
}
return JS_AND_ASSET_TYPES;
if ((module.buildInfo && module.buildInfo.dataUrl) || this.emit === false) {
if (sourceTypes) {
if (sourceTypes.has("javascript") && sourceTypes.has("css")) {
return CSS_AND_JS_TYPES;
} else if (sourceTypes.has("javascript")) {
return JS_TYPES;
} else if (sourceTypes.has("css")) {
return CSS_TYPES;
}
}
return NO_TYPES;
}
if (sourceTypes) {
if (sourceTypes.has("javascript") && sourceTypes.has("css")) {
return JS_AND_CSS_AND_ASSET_TYPES;
} else if (sourceTypes.has("javascript")) {
return JS_AND_ASSET_TYPES;
} else if (sourceTypes.has("css")) {
return CSS_AND_ASSET_TYPES;
}
}
return ASSET_TYPES;
}
/**

View File

@ -165,6 +165,7 @@ class AssetModulesPlugin {
const AssetGenerator = getAssetGenerator();
return new AssetGenerator(
compilation.moduleGraph,
dataUrl,
filename,
publicPath,

View File

@ -114,7 +114,9 @@ class RawDataUrlModule extends Module {
new RawSource(`module.exports = ${JSON.stringify(this.url)};`)
);
const data = new Map();
data.set("url", this.urlBuffer);
data.set("url", {
javascript: this.urlBuffer
});
const runtimeRequirements = new Set();
runtimeRequirements.add(RuntimeGlobals.module);
return { sources, runtimeRequirements, data };

View File

@ -12,10 +12,12 @@ const ModuleDependency = require("./ModuleDependency");
/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../Module").CodeGenerationResult} CodeGenerationResult */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
/** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */
@ -133,7 +135,7 @@ CssUrlDependency.Template = class CssUrlDependencyTemplate extends (
switch (dep.urlType) {
case "string":
newValue = cssEscapeString(
runtimeTemplate.assetUrl({
this.assetUrl({
module,
codeGenerationResults
})
@ -141,7 +143,7 @@ CssUrlDependency.Template = class CssUrlDependencyTemplate extends (
break;
case "url":
newValue = `url(${cssEscapeString(
runtimeTemplate.assetUrl({
this.assetUrl({
module,
codeGenerationResults
})
@ -155,6 +157,26 @@ CssUrlDependency.Template = class CssUrlDependencyTemplate extends (
/** @type {string} */ (newValue)
);
}
/**
* @param {object} options options object
* @param {Module} options.module the module
* @param {RuntimeSpec=} options.runtime runtime
* @param {CodeGenerationResults} options.codeGenerationResults the code generation results
* @returns {string} the url of the asset
*/
assetUrl({ runtime, module, codeGenerationResults }) {
if (!module) {
return "data:,";
}
const codeGen = codeGenerationResults.get(module, runtime);
const data =
/** @type {NonNullable<CodeGenerationResult["data"]>} */
(codeGen.data);
const url = data.get("url");
if (!url || !url["css-url"]) return "data:,";
return url["css-url"];
}
};
makeSerializable(CssUrlDependency, "webpack/lib/dependencies/CssUrlDependency");

14
types.d.ts vendored
View File

@ -13362,20 +13362,6 @@ declare abstract class RuntimeTemplate {
*/
runtimeRequirements: Set<string>;
}): string;
assetUrl(__0: {
/**
* the module
*/
module: Module;
/**
* runtime
*/
runtime?: RuntimeSpec;
/**
* the code generation results
*/
codeGenerationResults: CodeGenerationResults;
}): string;
}
declare abstract class RuntimeValue {
fn: (arg0: {