GetChunkFilenameRuntimeModule.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const RuntimeGlobals = require("../RuntimeGlobals");
  6. const RuntimeModule = require("../RuntimeModule");
  7. const Template = require("../Template");
  8. const { first } = require("../util/SetHelpers");
  9. /** @typedef {import("../Chunk")} Chunk */
  10. /** @typedef {import("../Compilation")} Compilation */
  11. /** @typedef {import("../Compilation").AssetInfo} AssetInfo */
  12. /** @typedef {import("../Compilation").PathData} PathData */
  13. /** @typedef {function(PathData, AssetInfo=): string} FilenameFunction */
  14. class GetChunkFilenameRuntimeModule extends RuntimeModule {
  15. /**
  16. * @param {string} contentType the contentType to use the content hash for
  17. * @param {string} name kind of filename
  18. * @param {string} global function name to be assigned
  19. * @param {function(Chunk): string | FilenameFunction} getFilenameForChunk functor to get the filename or function
  20. * @param {boolean} allChunks when false, only async chunks are included
  21. */
  22. constructor(contentType, name, global, getFilenameForChunk, allChunks) {
  23. super(`get ${name} chunk filename`);
  24. this.contentType = contentType;
  25. this.global = global;
  26. this.getFilenameForChunk = getFilenameForChunk;
  27. this.allChunks = allChunks;
  28. this.dependentHash = true;
  29. }
  30. /**
  31. * @returns {string} runtime code
  32. */
  33. generate() {
  34. const {
  35. global,
  36. chunk,
  37. chunkGraph,
  38. contentType,
  39. getFilenameForChunk,
  40. allChunks,
  41. compilation
  42. } = this;
  43. const { runtimeTemplate } = compilation;
  44. /** @type {Map<string | FilenameFunction, Set<Chunk>>} */
  45. const chunkFilenames = new Map();
  46. let maxChunks = 0;
  47. /** @type {string} */
  48. let dynamicFilename;
  49. /**
  50. * @param {Chunk} c the chunk
  51. * @returns {void}
  52. */
  53. const addChunk = c => {
  54. const chunkFilename = getFilenameForChunk(c);
  55. if (chunkFilename) {
  56. let set = chunkFilenames.get(chunkFilename);
  57. if (set === undefined) {
  58. chunkFilenames.set(chunkFilename, (set = new Set()));
  59. }
  60. set.add(c);
  61. if (typeof chunkFilename === "string") {
  62. if (set.size < maxChunks) return;
  63. if (set.size === maxChunks) {
  64. if (chunkFilename.length < dynamicFilename.length) return;
  65. if (chunkFilename.length === dynamicFilename.length) {
  66. if (chunkFilename < dynamicFilename) return;
  67. }
  68. }
  69. maxChunks = set.size;
  70. dynamicFilename = chunkFilename;
  71. }
  72. }
  73. };
  74. /** @type {string[]} */
  75. const includedChunksMessages = [];
  76. if (allChunks) {
  77. includedChunksMessages.push("all chunks");
  78. for (const c of chunk.getAllReferencedChunks()) {
  79. addChunk(c);
  80. }
  81. } else {
  82. includedChunksMessages.push("async chunks");
  83. for (const c of chunk.getAllAsyncChunks()) {
  84. addChunk(c);
  85. }
  86. const includeEntries = chunkGraph
  87. .getTreeRuntimeRequirements(chunk)
  88. .has(RuntimeGlobals.ensureChunkIncludeEntries);
  89. if (includeEntries) {
  90. includedChunksMessages.push("sibling chunks for the entrypoint");
  91. for (const c of chunkGraph.getChunkEntryDependentChunksIterable(
  92. chunk
  93. )) {
  94. addChunk(c);
  95. }
  96. }
  97. }
  98. for (const entrypoint of chunk.getAllReferencedAsyncEntrypoints()) {
  99. addChunk(entrypoint.chunks[entrypoint.chunks.length - 1]);
  100. }
  101. /** @type {Map<string, Set<string | number>>} */
  102. const staticUrls = new Map();
  103. /** @type {Set<Chunk>} */
  104. const dynamicUrlChunks = new Set();
  105. /**
  106. * @param {Chunk} c the chunk
  107. * @param {string | FilenameFunction} chunkFilename the filename template for the chunk
  108. * @returns {void}
  109. */
  110. const addStaticUrl = (c, chunkFilename) => {
  111. /**
  112. * @param {string | number} value a value
  113. * @returns {string} string to put in quotes
  114. */
  115. const unquotedStringify = value => {
  116. const str = `${value}`;
  117. if (str.length >= 5 && str === `${c.id}`) {
  118. // This is shorter and generates the same result
  119. return '" + chunkId + "';
  120. }
  121. const s = JSON.stringify(str);
  122. return s.slice(1, s.length - 1);
  123. };
  124. const unquotedStringifyWithLength = value => length =>
  125. unquotedStringify(`${value}`.slice(0, length));
  126. const chunkFilenameValue =
  127. typeof chunkFilename === "function"
  128. ? JSON.stringify(
  129. chunkFilename({
  130. chunk: c,
  131. contentHashType: contentType
  132. })
  133. )
  134. : JSON.stringify(chunkFilename);
  135. const staticChunkFilename = compilation.getPath(chunkFilenameValue, {
  136. hash: `" + ${RuntimeGlobals.getFullHash}() + "`,
  137. hashWithLength: length =>
  138. `" + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + "`,
  139. chunk: {
  140. id: unquotedStringify(c.id),
  141. hash: unquotedStringify(c.renderedHash),
  142. hashWithLength: unquotedStringifyWithLength(c.renderedHash),
  143. name: unquotedStringify(c.name || c.id),
  144. contentHash: {
  145. [contentType]: unquotedStringify(c.contentHash[contentType])
  146. },
  147. contentHashWithLength: {
  148. [contentType]: unquotedStringifyWithLength(
  149. c.contentHash[contentType]
  150. )
  151. }
  152. },
  153. contentHashType: contentType
  154. });
  155. let set = staticUrls.get(staticChunkFilename);
  156. if (set === undefined) {
  157. staticUrls.set(staticChunkFilename, (set = new Set()));
  158. }
  159. set.add(c.id);
  160. };
  161. for (const [filename, chunks] of chunkFilenames) {
  162. if (filename !== dynamicFilename) {
  163. for (const c of chunks) addStaticUrl(c, filename);
  164. } else {
  165. for (const c of chunks) dynamicUrlChunks.add(c);
  166. }
  167. }
  168. /**
  169. * @param {function(Chunk): string | number} fn function from chunk to value
  170. * @returns {string} code with static mapping of results of fn
  171. */
  172. const createMap = fn => {
  173. const obj = {};
  174. let useId = false;
  175. let lastKey;
  176. let entries = 0;
  177. for (const c of dynamicUrlChunks) {
  178. const value = fn(c);
  179. if (value === c.id) {
  180. useId = true;
  181. } else {
  182. obj[c.id] = value;
  183. lastKey = c.id;
  184. entries++;
  185. }
  186. }
  187. if (entries === 0) return "chunkId";
  188. if (entries === 1) {
  189. return useId
  190. ? `(chunkId === ${JSON.stringify(lastKey)} ? ${JSON.stringify(
  191. obj[lastKey]
  192. )} : chunkId)`
  193. : JSON.stringify(obj[lastKey]);
  194. }
  195. return useId
  196. ? `(${JSON.stringify(obj)}[chunkId] || chunkId)`
  197. : `${JSON.stringify(obj)}[chunkId]`;
  198. };
  199. /**
  200. * @param {function(Chunk): string | number} fn function from chunk to value
  201. * @returns {string} code with static mapping of results of fn for including in quoted string
  202. */
  203. const mapExpr = fn => {
  204. return `" + ${createMap(fn)} + "`;
  205. };
  206. /**
  207. * @param {function(Chunk): string | number} fn function from chunk to value
  208. * @returns {function(number): string} function which generates code with static mapping of results of fn for including in quoted string for specific length
  209. */
  210. const mapExprWithLength = fn => length => {
  211. return `" + ${createMap(c => `${fn(c)}`.slice(0, length))} + "`;
  212. };
  213. const url =
  214. dynamicFilename &&
  215. compilation.getPath(JSON.stringify(dynamicFilename), {
  216. hash: `" + ${RuntimeGlobals.getFullHash}() + "`,
  217. hashWithLength: length =>
  218. `" + ${RuntimeGlobals.getFullHash}().slice(0, ${length}) + "`,
  219. chunk: {
  220. id: `" + chunkId + "`,
  221. hash: mapExpr(c => c.renderedHash),
  222. hashWithLength: mapExprWithLength(c => c.renderedHash),
  223. name: mapExpr(c => c.name || c.id),
  224. contentHash: {
  225. [contentType]: mapExpr(c => c.contentHash[contentType])
  226. },
  227. contentHashWithLength: {
  228. [contentType]: mapExprWithLength(c => c.contentHash[contentType])
  229. }
  230. },
  231. contentHashType: contentType
  232. });
  233. return Template.asString([
  234. `// This function allow to reference ${includedChunksMessages.join(
  235. " and "
  236. )}`,
  237. `${global} = ${runtimeTemplate.basicFunction(
  238. "chunkId",
  239. staticUrls.size > 0
  240. ? [
  241. "// return url for filenames not based on template",
  242. // it minimizes to `x===1?"...":x===2?"...":"..."`
  243. Template.asString(
  244. Array.from(staticUrls, ([url, ids]) => {
  245. const condition =
  246. ids.size === 1
  247. ? `chunkId === ${JSON.stringify(first(ids))}`
  248. : `{${Array.from(
  249. ids,
  250. id => `${JSON.stringify(id)}:1`
  251. ).join(",")}}[chunkId]`;
  252. return `if (${condition}) return ${url};`;
  253. })
  254. ),
  255. "// return url for filenames based on template",
  256. `return ${url};`
  257. ]
  258. : ["// return url for filenames based on template", `return ${url};`]
  259. )};`
  260. ]);
  261. }
  262. }
  263. module.exports = GetChunkFilenameRuntimeModule;