AssetGenerator.js 15 KB

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