AssetGenerator.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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 ConcatenationScope = require("../ConcatenationScope");
  10. const Generator = require("../Generator");
  11. const RuntimeGlobals = require("../RuntimeGlobals");
  12. const createHash = require("../util/createHash");
  13. const { makePathsRelative } = require("../util/identifier");
  14. const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
  15. /** @typedef {import("webpack-sources").Source} Source */
  16. /** @typedef {import("../../declarations/WebpackOptions").AssetGeneratorOptions} AssetGeneratorOptions */
  17. /** @typedef {import("../../declarations/WebpackOptions").AssetModuleOutputPath} AssetModuleOutputPath */
  18. /** @typedef {import("../../declarations/WebpackOptions").RawPublicPath} RawPublicPath */
  19. /** @typedef {import("../Compilation")} Compilation */
  20. /** @typedef {import("../Compiler")} Compiler */
  21. /** @typedef {import("../Generator").GenerateContext} GenerateContext */
  22. /** @typedef {import("../Generator").UpdateHashContext} UpdateHashContext */
  23. /** @typedef {import("../Module")} Module */
  24. /** @typedef {import("../Module").ConcatenationBailoutReasonContext} ConcatenationBailoutReasonContext */
  25. /** @typedef {import("../NormalModule")} NormalModule */
  26. /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
  27. /** @typedef {import("../util/Hash")} Hash */
  28. const mergeMaybeArrays = (a, b) => {
  29. const set = new Set();
  30. if (Array.isArray(a)) for (const item of a) set.add(item);
  31. else set.add(a);
  32. if (Array.isArray(b)) for (const item of b) set.add(item);
  33. else set.add(b);
  34. return Array.from(set);
  35. };
  36. const mergeAssetInfo = (a, b) => {
  37. const result = { ...a, ...b };
  38. for (const key of Object.keys(a)) {
  39. if (key in b) {
  40. if (a[key] === b[key]) continue;
  41. switch (key) {
  42. case "fullhash":
  43. case "chunkhash":
  44. case "modulehash":
  45. case "contenthash":
  46. result[key] = mergeMaybeArrays(a[key], b[key]);
  47. break;
  48. case "immutable":
  49. case "development":
  50. case "hotModuleReplacement":
  51. case "javascriptModule":
  52. result[key] = a[key] || b[key];
  53. break;
  54. case "related":
  55. result[key] = mergeRelatedInfo(a[key], b[key]);
  56. break;
  57. default:
  58. throw new Error(`Can't handle conflicting asset info for ${key}`);
  59. }
  60. }
  61. }
  62. return result;
  63. };
  64. const mergeRelatedInfo = (a, b) => {
  65. const result = { ...a, ...b };
  66. for (const key of Object.keys(a)) {
  67. if (key in b) {
  68. if (a[key] === b[key]) continue;
  69. result[key] = mergeMaybeArrays(a[key], b[key]);
  70. }
  71. }
  72. return result;
  73. };
  74. const encodeDataUri = (encoding, source) => {
  75. let encodedContent;
  76. switch (encoding) {
  77. case "base64": {
  78. encodedContent = source.buffer().toString("base64");
  79. break;
  80. }
  81. case false: {
  82. const content = source.source();
  83. if (typeof content !== "string") {
  84. encodedContent = content.toString("utf-8");
  85. }
  86. encodedContent = encodeURIComponent(encodedContent).replace(
  87. /[!'()*]/g,
  88. character => "%" + character.codePointAt(0).toString(16)
  89. );
  90. break;
  91. }
  92. default:
  93. throw new Error(`Unsupported encoding '${encoding}'`);
  94. }
  95. return encodedContent;
  96. };
  97. const decodeDataUriContent = (encoding, content) => {
  98. const isBase64 = encoding === "base64";
  99. return isBase64
  100. ? Buffer.from(content, "base64")
  101. : Buffer.from(decodeURIComponent(content), "ascii");
  102. };
  103. const JS_TYPES = new Set(["javascript"]);
  104. const JS_AND_ASSET_TYPES = new Set(["javascript", "asset"]);
  105. const DEFAULT_ENCODING = "base64";
  106. class AssetGenerator extends Generator {
  107. /**
  108. * @param {AssetGeneratorOptions["dataUrl"]=} dataUrlOptions the options for the data url
  109. * @param {string=} filename override for output.assetModuleFilename
  110. * @param {RawPublicPath=} publicPath override for output.assetModulePublicPath
  111. * @param {AssetModuleOutputPath=} outputPath the output path for the emitted file which is not included in the runtime import
  112. * @param {boolean=} emit generate output asset
  113. */
  114. constructor(dataUrlOptions, filename, publicPath, outputPath, emit) {
  115. super();
  116. this.dataUrlOptions = dataUrlOptions;
  117. this.filename = filename;
  118. this.publicPath = publicPath;
  119. this.outputPath = outputPath;
  120. this.emit = emit;
  121. }
  122. /**
  123. * @param {NormalModule} module module
  124. * @param {RuntimeTemplate} runtimeTemplate runtime template
  125. * @returns {string} source file name
  126. */
  127. getSourceFileName(module, runtimeTemplate) {
  128. return makePathsRelative(
  129. runtimeTemplate.compilation.compiler.context,
  130. module.matchResource || module.resource,
  131. runtimeTemplate.compilation.compiler.root
  132. ).replace(/^\.\//, "");
  133. }
  134. /**
  135. * @param {NormalModule} module module for which the bailout reason should be determined
  136. * @param {ConcatenationBailoutReasonContext} context context
  137. * @returns {string | undefined} reason why this module can't be concatenated, undefined when it can be concatenated
  138. */
  139. getConcatenationBailoutReason(module, context) {
  140. return undefined;
  141. }
  142. /**
  143. * @param {NormalModule} module module
  144. * @returns {string} mime type
  145. */
  146. getMimeType(module) {
  147. if (typeof this.dataUrlOptions === "function") {
  148. throw new Error(
  149. "This method must not be called when dataUrlOptions is a function"
  150. );
  151. }
  152. let mimeType = this.dataUrlOptions.mimetype;
  153. if (mimeType === undefined) {
  154. const ext = path.extname(module.nameForCondition());
  155. if (
  156. module.resourceResolveData &&
  157. module.resourceResolveData.mimetype !== undefined
  158. ) {
  159. mimeType =
  160. module.resourceResolveData.mimetype +
  161. module.resourceResolveData.parameters;
  162. } else if (ext) {
  163. mimeType = mimeTypes.lookup(ext);
  164. if (typeof mimeType !== "string") {
  165. throw new Error(
  166. "DataUrl can't be generated automatically, " +
  167. `because there is no mimetype for "${ext}" in mimetype database. ` +
  168. 'Either pass a mimetype via "generator.mimetype" or ' +
  169. 'use type: "asset/resource" to create a resource file instead of a DataUrl'
  170. );
  171. }
  172. }
  173. }
  174. if (typeof mimeType !== "string") {
  175. throw new Error(
  176. "DataUrl can't be generated automatically. " +
  177. 'Either pass a mimetype via "generator.mimetype" or ' +
  178. 'use type: "asset/resource" to create a resource file instead of a DataUrl'
  179. );
  180. }
  181. return mimeType;
  182. }
  183. /**
  184. * @param {NormalModule} module module for which the code should be generated
  185. * @param {GenerateContext} generateContext context for generate
  186. * @returns {Source} generated code
  187. */
  188. generate(
  189. module,
  190. {
  191. runtime,
  192. concatenationScope,
  193. chunkGraph,
  194. runtimeTemplate,
  195. runtimeRequirements,
  196. type,
  197. getData
  198. }
  199. ) {
  200. switch (type) {
  201. case "asset":
  202. return module.originalSource();
  203. default: {
  204. let content;
  205. const originalSource = module.originalSource();
  206. if (module.buildInfo.dataUrl) {
  207. let encodedSource;
  208. if (typeof this.dataUrlOptions === "function") {
  209. encodedSource = this.dataUrlOptions.call(
  210. null,
  211. originalSource.source(),
  212. {
  213. filename: module.matchResource || module.resource,
  214. module
  215. }
  216. );
  217. } else {
  218. /** @type {string | false | undefined} */
  219. let encoding = this.dataUrlOptions.encoding;
  220. if (encoding === undefined) {
  221. if (
  222. module.resourceResolveData &&
  223. module.resourceResolveData.encoding !== undefined
  224. ) {
  225. encoding = module.resourceResolveData.encoding;
  226. }
  227. }
  228. if (encoding === undefined) {
  229. encoding = DEFAULT_ENCODING;
  230. }
  231. const mimeType = this.getMimeType(module);
  232. let encodedContent;
  233. if (
  234. module.resourceResolveData &&
  235. module.resourceResolveData.encoding === encoding &&
  236. decodeDataUriContent(
  237. module.resourceResolveData.encoding,
  238. module.resourceResolveData.encodedContent
  239. ).equals(originalSource.buffer())
  240. ) {
  241. encodedContent = module.resourceResolveData.encodedContent;
  242. } else {
  243. encodedContent = encodeDataUri(encoding, originalSource);
  244. }
  245. encodedSource = `data:${mimeType}${
  246. encoding ? `;${encoding}` : ""
  247. },${encodedContent}`;
  248. }
  249. const data = getData();
  250. data.set("url", Buffer.from(encodedSource));
  251. content = JSON.stringify(encodedSource);
  252. } else {
  253. const assetModuleFilename =
  254. this.filename || runtimeTemplate.outputOptions.assetModuleFilename;
  255. const hash = createHash(runtimeTemplate.outputOptions.hashFunction);
  256. if (runtimeTemplate.outputOptions.hashSalt) {
  257. hash.update(runtimeTemplate.outputOptions.hashSalt);
  258. }
  259. hash.update(originalSource.buffer());
  260. const fullHash = /** @type {string} */ (
  261. hash.digest(runtimeTemplate.outputOptions.hashDigest)
  262. );
  263. const contentHash = nonNumericOnlyHash(
  264. fullHash,
  265. runtimeTemplate.outputOptions.hashDigestLength
  266. );
  267. module.buildInfo.fullContentHash = fullHash;
  268. const sourceFilename = this.getSourceFileName(
  269. module,
  270. runtimeTemplate
  271. );
  272. let { path: filename, info: assetInfo } =
  273. runtimeTemplate.compilation.getAssetPathWithInfo(
  274. assetModuleFilename,
  275. {
  276. module,
  277. runtime,
  278. filename: sourceFilename,
  279. chunkGraph,
  280. contentHash
  281. }
  282. );
  283. let assetPath;
  284. if (this.publicPath !== undefined) {
  285. const { path, info } =
  286. runtimeTemplate.compilation.getAssetPathWithInfo(
  287. this.publicPath,
  288. {
  289. module,
  290. runtime,
  291. filename: sourceFilename,
  292. chunkGraph,
  293. contentHash
  294. }
  295. );
  296. assetInfo = mergeAssetInfo(assetInfo, info);
  297. assetPath = JSON.stringify(path + filename);
  298. } else {
  299. runtimeRequirements.add(RuntimeGlobals.publicPath); // add __webpack_require__.p
  300. assetPath = runtimeTemplate.concatenation(
  301. { expr: RuntimeGlobals.publicPath },
  302. filename
  303. );
  304. }
  305. assetInfo = {
  306. sourceFilename,
  307. ...assetInfo
  308. };
  309. if (this.outputPath) {
  310. const { path: outputPath, info } =
  311. runtimeTemplate.compilation.getAssetPathWithInfo(
  312. this.outputPath,
  313. {
  314. module,
  315. runtime,
  316. filename: sourceFilename,
  317. chunkGraph,
  318. contentHash
  319. }
  320. );
  321. assetInfo = mergeAssetInfo(assetInfo, info);
  322. filename = path.posix.join(outputPath, filename);
  323. }
  324. module.buildInfo.filename = filename;
  325. module.buildInfo.assetInfo = assetInfo;
  326. if (getData) {
  327. // Due to code generation caching module.buildInfo.XXX can't used to store such information
  328. // It need to be stored in the code generation results instead, where it's cached too
  329. // TODO webpack 6 For back-compat reasons we also store in on module.buildInfo
  330. const data = getData();
  331. data.set("fullContentHash", fullHash);
  332. data.set("filename", filename);
  333. data.set("assetInfo", assetInfo);
  334. }
  335. content = assetPath;
  336. }
  337. if (concatenationScope) {
  338. concatenationScope.registerNamespaceExport(
  339. ConcatenationScope.NAMESPACE_OBJECT_EXPORT
  340. );
  341. return new RawSource(
  342. `${runtimeTemplate.supportsConst() ? "const" : "var"} ${
  343. ConcatenationScope.NAMESPACE_OBJECT_EXPORT
  344. } = ${content};`
  345. );
  346. } else {
  347. runtimeRequirements.add(RuntimeGlobals.module);
  348. return new RawSource(
  349. `${RuntimeGlobals.module}.exports = ${content};`
  350. );
  351. }
  352. }
  353. }
  354. }
  355. /**
  356. * @param {NormalModule} module fresh module
  357. * @returns {Set<string>} available types (do not mutate)
  358. */
  359. getTypes(module) {
  360. if ((module.buildInfo && module.buildInfo.dataUrl) || this.emit === false) {
  361. return JS_TYPES;
  362. } else {
  363. return JS_AND_ASSET_TYPES;
  364. }
  365. }
  366. /**
  367. * @param {NormalModule} module the module
  368. * @param {string=} type source type
  369. * @returns {number} estimate size of the module
  370. */
  371. getSize(module, type) {
  372. switch (type) {
  373. case "asset": {
  374. const originalSource = module.originalSource();
  375. if (!originalSource) {
  376. return 0;
  377. }
  378. return originalSource.size();
  379. }
  380. default:
  381. if (module.buildInfo && module.buildInfo.dataUrl) {
  382. const originalSource = module.originalSource();
  383. if (!originalSource) {
  384. return 0;
  385. }
  386. // roughly for data url
  387. // Example: m.exports="data:image/png;base64,ag82/f+2=="
  388. // 4/3 = base64 encoding
  389. // 34 = ~ data url header + footer + rounding
  390. return originalSource.size() * 1.34 + 36;
  391. } else {
  392. // it's only estimated so this number is probably fine
  393. // Example: m.exports=r.p+"0123456789012345678901.ext"
  394. return 42;
  395. }
  396. }
  397. }
  398. /**
  399. * @param {Hash} hash hash that will be modified
  400. * @param {UpdateHashContext} updateHashContext context for updating hash
  401. */
  402. updateHash(hash, { module, runtime, runtimeTemplate, chunkGraph }) {
  403. if (module.buildInfo.dataUrl) {
  404. hash.update("data-url");
  405. // this.dataUrlOptions as function should be pure and only depend on input source and filename
  406. // therefore it doesn't need to be hashed
  407. if (typeof this.dataUrlOptions === "function") {
  408. const ident = /** @type {{ ident?: string }} */ (this.dataUrlOptions)
  409. .ident;
  410. if (ident) hash.update(ident);
  411. } else {
  412. if (
  413. this.dataUrlOptions.encoding &&
  414. this.dataUrlOptions.encoding !== DEFAULT_ENCODING
  415. ) {
  416. hash.update(this.dataUrlOptions.encoding);
  417. }
  418. if (this.dataUrlOptions.mimetype)
  419. hash.update(this.dataUrlOptions.mimetype);
  420. // computed mimetype depends only on module filename which is already part of the hash
  421. }
  422. } else {
  423. hash.update("resource");
  424. const pathData = {
  425. module,
  426. runtime,
  427. filename: this.getSourceFileName(module, runtimeTemplate),
  428. chunkGraph,
  429. contentHash: runtimeTemplate.contentHashReplacement
  430. };
  431. if (typeof this.publicPath === "function") {
  432. hash.update("path");
  433. const assetInfo = {};
  434. hash.update(this.publicPath(pathData, assetInfo));
  435. hash.update(JSON.stringify(assetInfo));
  436. } else if (this.publicPath) {
  437. hash.update("path");
  438. hash.update(this.publicPath);
  439. } else {
  440. hash.update("no-path");
  441. }
  442. const assetModuleFilename =
  443. this.filename || runtimeTemplate.outputOptions.assetModuleFilename;
  444. const { path: filename, info } =
  445. runtimeTemplate.compilation.getAssetPathWithInfo(
  446. assetModuleFilename,
  447. pathData
  448. );
  449. hash.update(filename);
  450. hash.update(JSON.stringify(info));
  451. }
  452. }
  453. }
  454. module.exports = AssetGenerator;