GetChunkFilenameRuntimeModule.js 9.2 KB

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