AssetGenerator.js 15 KB

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