2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Sergey Melyukov @smelukov
8 const mimeTypes = require("mime-types");
9 const path = require("path");
10 const { RawSource } = require("webpack-sources");
11 const ConcatenationScope = require("../ConcatenationScope");
12 const Generator = require("../Generator");
13 const RuntimeGlobals = require("../RuntimeGlobals");
14 const createHash = require("../util/createHash");
15 const { makePathsRelative } = require("../util/identifier");
16 const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
18 /** @typedef {import("webpack-sources").Source} Source */
19 /** @typedef {import("../../declarations/WebpackOptions").AssetGeneratorOptions} AssetGeneratorOptions */
20 /** @typedef {import("../../declarations/WebpackOptions").AssetModuleOutputPath} AssetModuleOutputPath */
21 /** @typedef {import("../../declarations/WebpackOptions").RawPublicPath} RawPublicPath */
22 /** @typedef {import("../Compilation")} Compilation */
23 /** @typedef {import("../Compiler")} Compiler */
24 /** @typedef {import("../Generator").GenerateContext} GenerateContext */
25 /** @typedef {import("../Generator").UpdateHashContext} UpdateHashContext */
26 /** @typedef {import("../Module")} Module */
27 /** @typedef {import("../Module").ConcatenationBailoutReasonContext} ConcatenationBailoutReasonContext */
28 /** @typedef {import("../NormalModule")} NormalModule */
29 /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
30 /** @typedef {import("../util/Hash")} Hash */
32 const mergeMaybeArrays = (a, b) => {
33 const set = new Set();
34 if (Array.isArray(a)) for (const item of a) set.add(item);
36 if (Array.isArray(b)) for (const item of b) set.add(item);
38 return Array.from(set);
41 const mergeAssetInfo = (a, b) => {
42 const result = { ...a, ...b };
43 for (const key of Object.keys(a)) {
45 if (a[key] === b[key]) continue;
51 result[key] = mergeMaybeArrays(a[key], b[key]);
55 case "hotModuleReplacement":
56 case "javascriptModule":
57 result[key] = a[key] || b[key];
60 result[key] = mergeRelatedInfo(a[key], b[key]);
63 throw new Error(`Can't handle conflicting asset info for ${key}`);
70 const mergeRelatedInfo = (a, b) => {
71 const result = { ...a, ...b };
72 for (const key of Object.keys(a)) {
74 if (a[key] === b[key]) continue;
75 result[key] = mergeMaybeArrays(a[key], b[key]);
81 const encodeDataUri = (encoding, source) => {
86 encodedContent = source.buffer().toString("base64");
90 const content = source.source();
92 if (typeof content !== "string") {
93 encodedContent = content.toString("utf-8");
96 encodedContent = encodeURIComponent(encodedContent).replace(
98 character => "%" + character.codePointAt(0).toString(16)
103 throw new Error(`Unsupported encoding '${encoding}'`);
106 return encodedContent;
109 const decodeDataUriContent = (encoding, content) => {
110 const isBase64 = encoding === "base64";
112 ? Buffer.from(content, "base64")
113 : Buffer.from(decodeURIComponent(content), "ascii");
116 const JS_TYPES = new Set(["javascript"]);
117 const JS_AND_ASSET_TYPES = new Set(["javascript", "asset"]);
118 const DEFAULT_ENCODING = "base64";
120 class AssetGenerator extends Generator {
122 * @param {AssetGeneratorOptions["dataUrl"]=} dataUrlOptions the options for the data url
123 * @param {string=} filename override for output.assetModuleFilename
124 * @param {RawPublicPath=} publicPath override for output.assetModulePublicPath
125 * @param {AssetModuleOutputPath=} outputPath the output path for the emitted file which is not included in the runtime import
126 * @param {boolean=} emit generate output asset
128 constructor(dataUrlOptions, filename, publicPath, outputPath, emit) {
130 this.dataUrlOptions = dataUrlOptions;
131 this.filename = filename;
132 this.publicPath = publicPath;
133 this.outputPath = outputPath;
138 * @param {NormalModule} module module
139 * @param {RuntimeTemplate} runtimeTemplate runtime template
140 * @returns {string} source file name
142 getSourceFileName(module, runtimeTemplate) {
143 return makePathsRelative(
144 runtimeTemplate.compilation.compiler.context,
145 module.matchResource || module.resource,
146 runtimeTemplate.compilation.compiler.root
147 ).replace(/^\.\//, "");
151 * @param {NormalModule} module module for which the bailout reason should be determined
152 * @param {ConcatenationBailoutReasonContext} context context
153 * @returns {string | undefined} reason why this module can't be concatenated, undefined when it can be concatenated
155 getConcatenationBailoutReason(module, context) {
160 * @param {NormalModule} module module
161 * @returns {string} mime type
163 getMimeType(module) {
164 if (typeof this.dataUrlOptions === "function") {
166 "This method must not be called when dataUrlOptions is a function"
170 let mimeType = this.dataUrlOptions.mimetype;
171 if (mimeType === undefined) {
172 const ext = path.extname(module.nameForCondition());
174 module.resourceResolveData &&
175 module.resourceResolveData.mimetype !== undefined
178 module.resourceResolveData.mimetype +
179 module.resourceResolveData.parameters;
181 mimeType = mimeTypes.lookup(ext);
183 if (typeof mimeType !== "string") {
185 "DataUrl can't be generated automatically, " +
186 `because there is no mimetype for "${ext}" in mimetype database. ` +
187 'Either pass a mimetype via "generator.mimetype" or ' +
188 'use type: "asset/resource" to create a resource file instead of a DataUrl'
194 if (typeof mimeType !== "string") {
196 "DataUrl can't be generated automatically. " +
197 'Either pass a mimetype via "generator.mimetype" or ' +
198 'use type: "asset/resource" to create a resource file instead of a DataUrl'
206 * @param {NormalModule} module module for which the code should be generated
207 * @param {GenerateContext} generateContext context for generate
208 * @returns {Source} generated code
224 return module.originalSource();
227 const originalSource = module.originalSource();
228 if (module.buildInfo.dataUrl) {
230 if (typeof this.dataUrlOptions === "function") {
231 encodedSource = this.dataUrlOptions.call(
233 originalSource.source(),
235 filename: module.matchResource || module.resource,
240 /** @type {string | false | undefined} */
241 let encoding = this.dataUrlOptions.encoding;
242 if (encoding === undefined) {
244 module.resourceResolveData &&
245 module.resourceResolveData.encoding !== undefined
247 encoding = module.resourceResolveData.encoding;
250 if (encoding === undefined) {
251 encoding = DEFAULT_ENCODING;
253 const mimeType = this.getMimeType(module);
258 module.resourceResolveData &&
259 module.resourceResolveData.encoding === encoding &&
260 decodeDataUriContent(
261 module.resourceResolveData.encoding,
262 module.resourceResolveData.encodedContent
263 ).equals(originalSource.buffer())
265 encodedContent = module.resourceResolveData.encodedContent;
267 encodedContent = encodeDataUri(encoding, originalSource);
270 encodedSource = `data:${mimeType}${
271 encoding ? `;${encoding}` : ""
272 },${encodedContent}`;
274 const data = getData();
275 data.set("url", Buffer.from(encodedSource));
276 content = JSON.stringify(encodedSource);
278 const assetModuleFilename =
279 this.filename || runtimeTemplate.outputOptions.assetModuleFilename;
280 const hash = createHash(runtimeTemplate.outputOptions.hashFunction);
281 if (runtimeTemplate.outputOptions.hashSalt) {
282 hash.update(runtimeTemplate.outputOptions.hashSalt);
284 hash.update(originalSource.buffer());
285 const fullHash = /** @type {string} */ (
286 hash.digest(runtimeTemplate.outputOptions.hashDigest)
288 const contentHash = nonNumericOnlyHash(
290 runtimeTemplate.outputOptions.hashDigestLength
292 module.buildInfo.fullContentHash = fullHash;
293 const sourceFilename = this.getSourceFileName(
297 let { path: filename, info: assetInfo } =
298 runtimeTemplate.compilation.getAssetPathWithInfo(
303 filename: sourceFilename,
309 if (this.publicPath !== undefined) {
310 const { path, info } =
311 runtimeTemplate.compilation.getAssetPathWithInfo(
316 filename: sourceFilename,
321 assetInfo = mergeAssetInfo(assetInfo, info);
322 assetPath = JSON.stringify(path + filename);
324 runtimeRequirements.add(RuntimeGlobals.publicPath); // add __webpack_require__.p
325 assetPath = runtimeTemplate.concatenation(
326 { expr: RuntimeGlobals.publicPath },
334 if (this.outputPath) {
335 const { path: outputPath, info } =
336 runtimeTemplate.compilation.getAssetPathWithInfo(
341 filename: sourceFilename,
346 assetInfo = mergeAssetInfo(assetInfo, info);
347 filename = path.posix.join(outputPath, filename);
349 module.buildInfo.filename = filename;
350 module.buildInfo.assetInfo = assetInfo;
352 // Due to code generation caching module.buildInfo.XXX can't used to store such information
353 // It need to be stored in the code generation results instead, where it's cached too
354 // TODO webpack 6 For back-compat reasons we also store in on module.buildInfo
355 const data = getData();
356 data.set("fullContentHash", fullHash);
357 data.set("filename", filename);
358 data.set("assetInfo", assetInfo);
363 if (concatenationScope) {
364 concatenationScope.registerNamespaceExport(
365 ConcatenationScope.NAMESPACE_OBJECT_EXPORT
367 return new RawSource(
368 `${runtimeTemplate.supportsConst() ? "const" : "var"} ${
369 ConcatenationScope.NAMESPACE_OBJECT_EXPORT
373 runtimeRequirements.add(RuntimeGlobals.module);
374 return new RawSource(
375 `${RuntimeGlobals.module}.exports = ${content};`
383 * @param {NormalModule} module fresh module
384 * @returns {Set<string>} available types (do not mutate)
387 if ((module.buildInfo && module.buildInfo.dataUrl) || this.emit === false) {
390 return JS_AND_ASSET_TYPES;
395 * @param {NormalModule} module the module
396 * @param {string=} type source type
397 * @returns {number} estimate size of the module
399 getSize(module, type) {
402 const originalSource = module.originalSource();
404 if (!originalSource) {
408 return originalSource.size();
411 if (module.buildInfo && module.buildInfo.dataUrl) {
412 const originalSource = module.originalSource();
414 if (!originalSource) {
418 // roughly for data url
419 // Example: m.exports="data:image/png;base64,ag82/f+2=="
420 // 4/3 = base64 encoding
421 // 34 = ~ data url header + footer + rounding
422 return originalSource.size() * 1.34 + 36;
424 // it's only estimated so this number is probably fine
425 // Example: m.exports=r.p+"0123456789012345678901.ext"
432 * @param {Hash} hash hash that will be modified
433 * @param {UpdateHashContext} updateHashContext context for updating hash
435 updateHash(hash, { module, runtime, runtimeTemplate, chunkGraph }) {
436 if (module.buildInfo.dataUrl) {
437 hash.update("data-url");
438 // this.dataUrlOptions as function should be pure and only depend on input source and filename
439 // therefore it doesn't need to be hashed
440 if (typeof this.dataUrlOptions === "function") {
441 const ident = /** @type {{ ident?: string }} */ (this.dataUrlOptions)
443 if (ident) hash.update(ident);
446 this.dataUrlOptions.encoding &&
447 this.dataUrlOptions.encoding !== DEFAULT_ENCODING
449 hash.update(this.dataUrlOptions.encoding);
451 if (this.dataUrlOptions.mimetype)
452 hash.update(this.dataUrlOptions.mimetype);
453 // computed mimetype depends only on module filename which is already part of the hash
456 hash.update("resource");
461 filename: this.getSourceFileName(module, runtimeTemplate),
463 contentHash: runtimeTemplate.contentHashReplacement
466 if (typeof this.publicPath === "function") {
468 const assetInfo = {};
469 hash.update(this.publicPath(pathData, assetInfo));
470 hash.update(JSON.stringify(assetInfo));
471 } else if (this.publicPath) {
473 hash.update(this.publicPath);
475 hash.update("no-path");
478 const assetModuleFilename =
479 this.filename || runtimeTemplate.outputOptions.assetModuleFilename;
480 const { path: filename, info } =
481 runtimeTemplate.compilation.getAssetPathWithInfo(
485 hash.update(filename);
486 hash.update(JSON.stringify(info));
491 module.exports = AssetGenerator;