GetChunkFilenameRuntimeModule.js 9.1 KB

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