webpack/lib/asset/AssetGenerator.js

358 lines
10 KiB
JavaScript

/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Sergey Melyukov @smelukov
*/
"use strict";
const mimeTypes = require("mime-types");
const path = require("path");
const { RawSource } = require("webpack-sources");
const Generator = require("../Generator");
const RuntimeGlobals = require("../RuntimeGlobals");
const createHash = require("../util/createHash");
const { makePathsRelative } = require("../util/identifier");
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../../declarations/WebpackOptions").AssetGeneratorOptions} AssetGeneratorOptions */
/** @typedef {import("../../declarations/WebpackOptions").RawPublicPath} RawPublicPath */
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Generator").GenerateContext} GenerateContext */
/** @typedef {import("../Generator").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../NormalModule")} NormalModule */
/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
/** @typedef {import("../util/Hash")} Hash */
const mergeMaybeArrays = (a, b) => {
const set = new Set();
if (Array.isArray(a)) for (const item of a) set.add(item);
else set.add(a);
if (Array.isArray(b)) for (const item of b) set.add(item);
else set.add(b);
return Array.from(set);
};
const mergeAssetInfo = (a, b) => {
const result = { ...a, ...b };
for (const key of Object.keys(a)) {
if (key in b) {
if (a[key] === b[key]) continue;
switch (key) {
case "fullhash":
case "chunkhash":
case "modulehash":
case "contenthash":
result[key] = mergeMaybeArrays(a[key], b[key]);
break;
case "immutable":
case "development":
case "hotModuleReplacement":
case "javascriptModule":
result[key] = a[key] || b[key];
break;
case "related":
result[key] = mergeRelatedInfo(a[key], b[key]);
break;
default:
throw new Error(`Can't handle conflicting asset info for ${key}`);
}
}
}
return result;
};
const mergeRelatedInfo = (a, b) => {
const result = { ...a, ...b };
for (const key of Object.keys(a)) {
if (key in b) {
if (a[key] === b[key]) continue;
result[key] = mergeMaybeArrays(a[key], b[key]);
}
}
return result;
};
const encodeDataUri = (encoding, source) => {
let encodedContent;
switch (encoding) {
case "base64": {
encodedContent = source.buffer().toString("base64");
break;
}
case false: {
const content = source.source();
if (typeof content !== "string") {
encodedContent = content.toString("utf-8");
}
encodedContent = encodeURIComponent(encodedContent).replace(
/[!'()*]/g,
character => "%" + character.codePointAt(0).toString(16)
);
break;
}
default:
throw new Error(`Unsupported encoding '${encoding}'`);
}
return encodedContent;
};
const decodeDataUriContent = (encoding, content) => {
const isBase64 = encoding === "base64";
return isBase64
? Buffer.from(content, "base64")
: Buffer.from(decodeURIComponent(content), "ascii");
};
const JS_TYPES = new Set(["javascript"]);
const JS_AND_ASSET_TYPES = new Set(["javascript", "asset"]);
class AssetGenerator extends Generator {
/**
* @param {AssetGeneratorOptions["dataUrl"]=} dataUrlOptions the options for the data url
* @param {string=} filename override for output.assetModuleFilename
* @param {RawPublicPath=} publicPath override for output.assetModulePublicPath
* @param {boolean=} emit generate output asset
*/
constructor(dataUrlOptions, filename, publicPath, emit) {
super();
this.dataUrlOptions = dataUrlOptions;
this.filename = filename;
this.publicPath = publicPath;
this.emit = emit;
}
/**
* @param {NormalModule} module module for which the code should be generated
* @param {GenerateContext} generateContext context for generate
* @returns {Source} generated code
*/
generate(
module,
{ runtime, chunkGraph, runtimeTemplate, runtimeRequirements, type, getData }
) {
switch (type) {
case "asset":
return module.originalSource();
default: {
runtimeRequirements.add(RuntimeGlobals.module);
const originalSource = module.originalSource();
if (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 {string | false | undefined} */
let encoding = this.dataUrlOptions.encoding;
if (encoding === undefined) {
if (
module.resourceResolveData &&
module.resourceResolveData.encoding !== undefined
) {
encoding = module.resourceResolveData.encoding;
}
}
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'
);
}
let encodedContent;
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);
}
encodedSource = `data:${mimeType}${
encoding ? `;${encoding}` : ""
},${encodedContent}`;
}
const data = getData();
data.set("url", encodedSource);
return new RawSource(
`${RuntimeGlobals.module}.exports = ${JSON.stringify(
encodedSource
)};`
);
} else {
const assetModuleFilename =
this.filename || runtimeTemplate.outputOptions.assetModuleFilename;
const hash = createHash(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 = fullHash.slice(
0,
runtimeTemplate.outputOptions.hashDigestLength
);
module.buildInfo.fullContentHash = fullHash;
const sourceFilename = makePathsRelative(
runtimeTemplate.compilation.compiler.context,
module.matchResource || module.resource,
runtimeTemplate.compilation.compiler.root
).replace(/^\.\//, "");
let { path: filename, info: assetInfo } =
runtimeTemplate.compilation.getAssetPathWithInfo(
assetModuleFilename,
{
module,
runtime,
filename: sourceFilename,
chunkGraph,
contentHash
}
);
let assetPath;
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);
} else {
runtimeRequirements.add(RuntimeGlobals.publicPath); // add __webpack_require__.p
assetPath = runtimeTemplate.concatenation(
{ expr: RuntimeGlobals.publicPath },
filename
);
}
assetInfo = {
sourceFilename,
...assetInfo
};
module.buildInfo.filename = filename;
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);
}
return new RawSource(
`${RuntimeGlobals.module}.exports = ${assetPath};`
);
}
}
}
}
/**
* @param {NormalModule} module fresh module
* @returns {Set<string>} available types (do not mutate)
*/
getTypes(module) {
if ((module.buildInfo && module.buildInfo.dataUrl) || this.emit === false) {
return JS_TYPES;
} else {
return JS_AND_ASSET_TYPES;
}
}
/**
* @param {NormalModule} module the module
* @param {string=} type source type
* @returns {number} estimate size of the module
*/
getSize(module, type) {
switch (type) {
case "asset": {
const originalSource = module.originalSource();
if (!originalSource) {
return 0;
}
return originalSource.size();
}
default:
if (module.buildInfo && module.buildInfo.dataUrl) {
const originalSource = module.originalSource();
if (!originalSource) {
return 0;
}
// roughly for data url
// Example: m.exports="data:image/png;base64,ag82/f+2=="
// 4/3 = base64 encoding
// 34 = ~ data url header + footer + rounding
return originalSource.size() * 1.34 + 36;
} else {
// it's only estimated so this number is probably fine
// Example: m.exports=r.p+"0123456789012345678901.ext"
return 42;
}
}
}
/**
* @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");
}
}
module.exports = AssetGenerator;