AssetGenerator.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Sergey Melyukov @smelukov
  4. */
  5. "use strict";
  6. const mimeTypes = require("mime-types");
  7. const path = require("path");
  8. const { RawSource } = require("webpack-sources");
  9. const Generator = require("../Generator");
  10. const RuntimeGlobals = require("../RuntimeGlobals");
  11. const createHash = require("../util/createHash");
  12. const { makePathsRelative } = require("../util/identifier");
  13. const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
  14. /** @typedef {import("webpack-sources").Source} Source */
  15. /** @typedef {import("../../declarations/WebpackOptions").AssetGeneratorOptions} AssetGeneratorOptions */
  16. /** @typedef {import("../../declarations/WebpackOptions").AssetModuleOutputPath} AssetModuleOutputPath */
  17. /** @typedef {import("../../declarations/WebpackOptions").RawPublicPath} RawPublicPath */
  18. /** @typedef {import("../Compilation")} Compilation */
  19. /** @typedef {import("../Compiler")} Compiler */
  20. /** @typedef {import("../Generator").GenerateContext} GenerateContext */
  21. /** @typedef {import("../Generator").UpdateHashContext} UpdateHashContext */
  22. /** @typedef {import("../Module")} Module */
  23. /** @typedef {import("../NormalModule")} NormalModule */
  24. /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
  25. /** @typedef {import("../util/Hash")} Hash */
  26. const mergeMaybeArrays = (a, b) => {
  27. const set = new Set();
  28. if (Array.isArray(a)) for (const item of a) set.add(item);
  29. else set.add(a);
  30. if (Array.isArray(b)) for (const item of b) set.add(item);
  31. else set.add(b);
  32. return Array.from(set);
  33. };
  34. const mergeAssetInfo = (a, b) => {
  35. const result = { ...a, ...b };
  36. for (const key of Object.keys(a)) {
  37. if (key in b) {
  38. if (a[key] === b[key]) continue;
  39. switch (key) {
  40. case "fullhash":
  41. case "chunkhash":
  42. case "modulehash":
  43. case "contenthash":
  44. result[key] = mergeMaybeArrays(a[key], b[key]);
  45. break;
  46. case "immutable":
  47. case "development":
  48. case "hotModuleReplacement":
  49. case "javascriptModule":
  50. result[key] = a[key] || b[key];
  51. break;
  52. case "related":
  53. result[key] = mergeRelatedInfo(a[key], b[key]);
  54. break;
  55. default:
  56. throw new Error(`Can't handle conflicting asset info for ${key}`);
  57. }
  58. }
  59. }
  60. return result;
  61. };
  62. const mergeRelatedInfo = (a, b) => {
  63. const result = { ...a, ...b };
  64. for (const key of Object.keys(a)) {
  65. if (key in b) {
  66. if (a[key] === b[key]) continue;
  67. result[key] = mergeMaybeArrays(a[key], b[key]);
  68. }
  69. }
  70. return result;
  71. };
  72. const encodeDataUri = (encoding, source) => {
  73. let encodedContent;
  74. switch (encoding) {
  75. case "base64": {
  76. encodedContent = source.buffer().toString("base64");
  77. break;
  78. }
  79. case false: {
  80. const content = source.source();
  81. if (typeof content !== "string") {
  82. encodedContent = content.toString("utf-8");
  83. }
  84. encodedContent = encodeURIComponent(encodedContent).replace(
  85. /[!'()*]/g,
  86. character => "%" + character.codePointAt(0).toString(16)
  87. );
  88. break;
  89. }
  90. default:
  91. throw new Error(`Unsupported encoding '${encoding}'`);
  92. }
  93. return encodedContent;
  94. };
  95. const decodeDataUriContent = (encoding, content) => {
  96. const isBase64 = encoding === "base64";
  97. return isBase64
  98. ? Buffer.from(content, "base64")
  99. : Buffer.from(decodeURIComponent(content), "ascii");
  100. };
  101. const JS_TYPES = new Set(["javascript"]);
  102. const JS_AND_ASSET_TYPES = new Set(["javascript", "asset"]);
  103. class AssetGenerator extends Generator {
  104. /**
  105. * @param {AssetGeneratorOptions["dataUrl"]=} dataUrlOptions the options for the data url
  106. * @param {string=} filename override for output.assetModuleFilename
  107. * @param {RawPublicPath=} publicPath override for output.assetModulePublicPath
  108. * @param {AssetModuleOutputPath=} outputPath the output path for the emitted file which is not included in the runtime import
  109. * @param {boolean=} emit generate output asset
  110. */
  111. constructor(dataUrlOptions, filename, publicPath, outputPath, emit) {
  112. super();
  113. this.dataUrlOptions = dataUrlOptions;
  114. this.filename = filename;
  115. this.publicPath = publicPath;
  116. this.outputPath = outputPath;
  117. this.emit = emit;
  118. }
  119. /**
  120. * @param {NormalModule} module module for which the code should be generated
  121. * @param {GenerateContext} generateContext context for generate
  122. * @returns {Source} generated code
  123. */
  124. generate(
  125. module,
  126. { runtime, chunkGraph, runtimeTemplate, runtimeRequirements, type, getData }
  127. ) {
  128. switch (type) {
  129. case "asset":
  130. return module.originalSource();
  131. default: {
  132. runtimeRequirements.add(RuntimeGlobals.module);
  133. const originalSource = module.originalSource();
  134. if (module.buildInfo.dataUrl) {
  135. let encodedSource;
  136. if (typeof this.dataUrlOptions === "function") {
  137. encodedSource = this.dataUrlOptions.call(
  138. null,
  139. originalSource.source(),
  140. {
  141. filename: module.matchResource || module.resource,
  142. module
  143. }
  144. );
  145. } else {
  146. /** @type {string | false | undefined} */
  147. let encoding = this.dataUrlOptions.encoding;
  148. if (encoding === undefined) {
  149. if (
  150. module.resourceResolveData &&
  151. module.resourceResolveData.encoding !== undefined
  152. ) {
  153. encoding = module.resourceResolveData.encoding;
  154. }
  155. }
  156. if (encoding === undefined) {
  157. encoding = "base64";
  158. }
  159. let ext;
  160. let mimeType = this.dataUrlOptions.mimetype;
  161. if (mimeType === undefined) {
  162. ext = path.extname(module.nameForCondition());
  163. if (
  164. module.resourceResolveData &&
  165. module.resourceResolveData.mimetype !== undefined
  166. ) {
  167. mimeType =
  168. module.resourceResolveData.mimetype +
  169. module.resourceResolveData.parameters;
  170. } else if (ext) {
  171. mimeType = mimeTypes.lookup(ext);
  172. }
  173. }
  174. if (typeof mimeType !== "string") {
  175. throw new Error(
  176. "DataUrl can't be generated automatically, " +
  177. `because there is no mimetype for "${ext}" in mimetype database. ` +
  178. 'Either pass a mimetype via "generator.mimetype" or ' +
  179. 'use type: "asset/resource" to create a resource file instead of a DataUrl'
  180. );
  181. }
  182. let encodedContent;
  183. if (
  184. module.resourceResolveData &&
  185. module.resourceResolveData.encoding === encoding &&
  186. decodeDataUriContent(
  187. module.resourceResolveData.encoding,
  188. module.resourceResolveData.encodedContent
  189. ).equals(originalSource.buffer())
  190. ) {
  191. encodedContent = module.resourceResolveData.encodedContent;
  192. } else {
  193. encodedContent = encodeDataUri(encoding, originalSource);
  194. }
  195. encodedSource = `data:${mimeType}${
  196. encoding ? `;${encoding}` : ""
  197. },${encodedContent}`;
  198. }
  199. const data = getData();
  200. data.set("url", Buffer.from(encodedSource));
  201. return new RawSource(
  202. `${RuntimeGlobals.module}.exports = ${JSON.stringify(
  203. encodedSource
  204. )};`
  205. );
  206. } else {
  207. const assetModuleFilename =
  208. this.filename || runtimeTemplate.outputOptions.assetModuleFilename;
  209. const hash = createHash(runtimeTemplate.outputOptions.hashFunction);
  210. if (runtimeTemplate.outputOptions.hashSalt) {
  211. hash.update(runtimeTemplate.outputOptions.hashSalt);
  212. }
  213. hash.update(originalSource.buffer());
  214. const fullHash = /** @type {string} */ (
  215. hash.digest(runtimeTemplate.outputOptions.hashDigest)
  216. );
  217. const contentHash = nonNumericOnlyHash(
  218. fullHash,
  219. runtimeTemplate.outputOptions.hashDigestLength
  220. );
  221. module.buildInfo.fullContentHash = fullHash;
  222. const sourceFilename = makePathsRelative(
  223. runtimeTemplate.compilation.compiler.context,
  224. module.matchResource || module.resource,
  225. runtimeTemplate.compilation.compiler.root
  226. ).replace(/^\.\//, "");
  227. let { path: filename, info: assetInfo } =
  228. runtimeTemplate.compilation.getAssetPathWithInfo(
  229. assetModuleFilename,
  230. {
  231. module,
  232. runtime,
  233. filename: sourceFilename,
  234. chunkGraph,
  235. contentHash
  236. }
  237. );
  238. let assetPath;
  239. if (this.publicPath !== undefined) {
  240. const { path, info } =
  241. runtimeTemplate.compilation.getAssetPathWithInfo(
  242. this.publicPath,
  243. {
  244. module,
  245. runtime,
  246. filename: sourceFilename,
  247. chunkGraph,
  248. contentHash
  249. }
  250. );
  251. assetInfo = mergeAssetInfo(assetInfo, info);
  252. assetPath = JSON.stringify(path + filename);
  253. } else {
  254. runtimeRequirements.add(RuntimeGlobals.publicPath); // add __webpack_require__.p
  255. assetPath = runtimeTemplate.concatenation(
  256. { expr: RuntimeGlobals.publicPath },
  257. filename
  258. );
  259. }
  260. assetInfo = {
  261. sourceFilename,
  262. ...assetInfo
  263. };
  264. if (this.outputPath) {
  265. const { path: outputPath, info } =
  266. runtimeTemplate.compilation.getAssetPathWithInfo(
  267. this.outputPath,
  268. {
  269. module,
  270. runtime,
  271. filename: sourceFilename,
  272. chunkGraph,
  273. contentHash
  274. }
  275. );
  276. assetInfo = mergeAssetInfo(assetInfo, info);
  277. filename = path.posix.join(outputPath, filename);
  278. }
  279. module.buildInfo.filename = filename;
  280. module.buildInfo.assetInfo = assetInfo;
  281. if (getData) {
  282. // Due to code generation caching module.buildInfo.XXX can't used to store such information
  283. // It need to be stored in the code generation results instead, where it's cached too
  284. // TODO webpack 6 For back-compat reasons we also store in on module.buildInfo
  285. const data = getData();
  286. data.set("fullContentHash", fullHash);
  287. data.set("filename", filename);
  288. data.set("assetInfo", assetInfo);
  289. }
  290. return new RawSource(
  291. `${RuntimeGlobals.module}.exports = ${assetPath};`
  292. );
  293. }
  294. }
  295. }
  296. }
  297. /**
  298. * @param {NormalModule} module fresh module
  299. * @returns {Set<string>} available types (do not mutate)
  300. */
  301. getTypes(module) {
  302. if ((module.buildInfo && module.buildInfo.dataUrl) || this.emit === false) {
  303. return JS_TYPES;
  304. } else {
  305. return JS_AND_ASSET_TYPES;
  306. }
  307. }
  308. /**
  309. * @param {NormalModule} module the module
  310. * @param {string=} type source type
  311. * @returns {number} estimate size of the module
  312. */
  313. getSize(module, type) {
  314. switch (type) {
  315. case "asset": {
  316. const originalSource = module.originalSource();
  317. if (!originalSource) {
  318. return 0;
  319. }
  320. return originalSource.size();
  321. }
  322. default:
  323. if (module.buildInfo && module.buildInfo.dataUrl) {
  324. const originalSource = module.originalSource();
  325. if (!originalSource) {
  326. return 0;
  327. }
  328. // roughly for data url
  329. // Example: m.exports="data:image/png;base64,ag82/f+2=="
  330. // 4/3 = base64 encoding
  331. // 34 = ~ data url header + footer + rounding
  332. return originalSource.size() * 1.34 + 36;
  333. } else {
  334. // it's only estimated so this number is probably fine
  335. // Example: m.exports=r.p+"0123456789012345678901.ext"
  336. return 42;
  337. }
  338. }
  339. }
  340. /**
  341. * @param {Hash} hash hash that will be modified
  342. * @param {UpdateHashContext} updateHashContext context for updating hash
  343. */
  344. updateHash(hash, { module }) {
  345. hash.update(module.buildInfo.dataUrl ? "data-url" : "resource");
  346. }
  347. }
  348. module.exports = AssetGenerator;