GetChunkFilenameRuntimeModule.js 9.2 KB

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