AssetGenerator.js 14 KB

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